Go未内置Observer接口,主张用函数值、channel和组合替代继承式抽象;推荐带缓冲channel异步分发、sync.RWMutex线程安全注册、context控制超时,避免内存泄漏。

Go 里没有内置 Observer 接口,得自己搭骨架
Go 语言标准库不提供 Observer 或 Observable 类型,不像 Java 的 java.util.Observer 或 C# 的 IObservable。这不是遗漏,是设计取向——Go 倾向用组合、函数值和 channel 显式表达通知逻辑,而不是抽象接口加继承。
直接套用其他语言的 Observer 模板(比如定义 Register()/Notify() 接口)容易写成“Java 风格 Go”,反而掩盖 channel 和闭包的天然优势。
实操建议:
- 用
map[*observer]func()或[]func()存回调,比自定义接口更轻、更易测试 - 避免在观察者方法里做阻塞操作(如 HTTP 调用、数据库写入),否则会拖慢整个通知流
- 如果需要顺序保证或错误传播,别依赖回调列表执行顺序——Go 不保证 map 遍历顺序,应改用切片
[]func()
用 channel 实现异步事件分发最稳妥
同步调用回调函数看似简单,但一旦某个观察者 panic 或卡住,整个通知就挂了。生产环境更推荐把事件推到 chan Event,由独立 goroutine 消费并分发。
立即学习“go语言免费学习笔记(深入)”;
常见错误现象:fatal error: all goroutines are asleep - deadlock,通常是因为 channel 无缓冲且没人接收,或忘记启动消费 goroutine。
使用场景:服务状态变更(如配置重载)、指标采集触发、审计日志广播。
实操建议:
- 声明带缓冲的 channel:
events := make(chan Event, 1024),防突发事件压垮内存 - 启动一个长期运行的 goroutine 处理分发:
go func() { for e := range events { ... } }() - 事件结构体字段尽量小,避免大对象拷贝;必要时传指针,但需确保观察者不会持久化引用导致 GC 延迟
注册/注销要线程安全,sync.RWMutex 比 mutex 更合适
多个 goroutine 同时调用 Register() 和 Unregister() 是常态。只用 sync.Mutex 会限制并发读性能——毕竟读(遍历观察者列表)远多于写(增删)。
参数差异:sync.RWMutex 提供 RLock()/RUnlock()(允许多读)和 Lock()/Unlock()(独占写),比纯 mutex 更贴合观察者模式读多写少的特点。
容易踩的坑:
- 在
Rlock()区域内调用可能阻塞的函数(如http.Get()),会导致其他读被锁住 - 误把
RUnlock()写成Unlock(),引发 panic:fatal error: sync: RUnlock of unlocked RWMutex - 注销时用
delete(map, key)但没加写锁,造成 concurrent map iteration and map write crash
Context 传递能解决超时和取消,但别滥用
观察者执行时间不可控,上游可能只想等 500ms。这时候靠 context.Context 传取消信号比让每个观察者自己开 timer 更干净。
性能影响:每次通知都新建 context.WithTimeout() 有小开销,但如果观察者本身涉及 I/O,这点分配成本可忽略。
实操建议:
- 通知函数签名建议为:
func(ctx context.Context, event Event) error,让观察者自行决定是否响应ctx.Done() - 不要在注册时绑定 context——context 生命周期短,注册后很快过期,导致后续通知失效
- 若某观察者必须同步完成(如写本地缓存),就别强加 context 取消;混合模型更现实:部分观察者支持 cancel,部分不支持
真正麻烦的不是怎么发通知,而是谁该负责清理已退出的观察者。HTTP handler 注册了回调,但 handler 返回后没注销,goroutine 还在往已失效的 channel 发数据——这种泄漏很难测出来,得靠 pprof + runtime.ReadMemStats 定期扫。










