Go中Observer应使用channel+sync.Map+显式token控制生命周期,避免goroutine泄漏和并发写panic;事件分发需异步、带缓冲channel防压垮内存;handler须带context超时且禁用interface{}签名。

Go 里没有 Observer 接口,硬套 Java 风格的 Register()/Notify() 会踩一堆坑——最常见的是 goroutine 泄漏、并发写 panic、回调阻塞整个通知链。真正靠谱的做法是用 channel + sync.Map + 显式 token 控制生命周期,而不是维护一个随时可能被并发修改的切片。
用带缓冲 chan Event 实现异步分发
同步调用所有回调函数看似简单,但只要某个 handler 里有 http.Get() 或没设超时的数据库查询,整个 Notify() 就卡住。生产环境必须异步。
- 声明带缓冲的 channel:
events := make(chan Event, 1024),防突发流量压垮内存 - 启动独立 goroutine 消费:
go func() { for e := range events { /* 分发逻辑 */ } }(),别漏掉这行,否则fatal error: all goroutines are asleep - deadlock - 事件结构体字段尽量小;大对象传指针,但确保观察者不持久化引用,否则 GC 延迟
- 别在
for range events循环里做耗时操作(如直接调log.Printf),应转发给 worker pool 处理
sync.Map 存 handler 而不是切片
用 []func(Event) 管理观察者,Attach()/Detach() 必须加锁,且遍历时不能删元素;sync.Map 天然适合读多写少的注册表场景,但要注意它不支持遍历中删除。
- 注册:用
store.Store(token, handler),token推荐string(如 UUID)或struct{},由调用方保管 - 触发前:
if h, ok := store.Load(token); ok { h.(func(Event)) (e) },别用map[string]func(Event)—— 并发写直接 panic - 需要动态注销?别在
Load后遍历中调Delete,改用带sync.RWMutex的自定义结构 - 测试时别 mock 全局
sync.Map,用done := make(chan struct{}, N)捕获 handler 是否被调用,稳定又快
回调函数签名别用 interface{}
很多示例写 func(event interface{}),看着灵活,实则埋雷:类型断言失败就 panic,而且编译期完全检查不出来。
立即学习“go语言免费学习笔记(深入)”;
- 为每个事件类型定义具体 handler 签名,比如
type UserCreatedHandler func(*UserCreatedEvent) - 注册表按事件名分桶:
map[string][]UserCreatedHandler,发布时强制类型匹配 - handler 内部若有网络调用,必须带
context.Context参数并设超时,否则一个慢请求拖死整条链 - 避免在 handler 里起
go func()—— 高频事件下 goroutine 积压,改用固定数量的 worker pool 消费chan func()
真正麻烦的不是怎么注册和发事件,而是 handler 里混了没 context 控制的 HTTP 调用、没超时的 DB 查询、或者忘了关连接——这些错误不会在编译时报出来,只会在压测或上线后让整个通知系统静默卡死。


















