Go可用接口+map+互斥锁轻量实现观察者模式,Observer定义Update方法,Subject用map[string]Observer管理并支持Attach/Detach,Notify异步分发事件,需结合context或规范解绑防泄漏。

Go 语言没有内置的 Observer 接口或事件总线,但可以用接口 + 切片 + 方法绑定的方式轻量实现观察者模式,关键在于避免循环引用和确保通知时的并发安全。
用 Observer 接口和 Subject 结构体定义核心契约
观察者必须实现统一的通知方法,被观察者(Subject)则维护观察者列表并提供注册/移除/通知能力。不要用泛型约束观察者类型,否则会限制使用场景;用接口更灵活。
-
Observer接口只定义一个Update(event interface{})方法,接受任意事件数据 -
Subject是普通结构体,字段包含observers []Observer和互斥锁mu sync.RWMutex - 注册方法
Attach(o Observer)需加写锁,避免并发 append 导致 panic - 通知方法
Notify(event interface{})用读锁遍历,但注意:不能在遍历时修改observers切片
避免在 Notify 中同步调用观察者导致阻塞或死锁
如果某个 Observer.Update() 执行耗时或调用了 Detach(),同步遍历会卡住整个通知流,还可能引发递归修改切片的 panic。生产环境必须异步化。
- 在
Notify内启动 goroutine 分发事件,但需控制并发数,避免 goroutine 泛滥 - 更稳妥的做法是把事件推入 channel,由单独的 dispatcher goroutine 消费
- 若观察者数量少且逻辑简单(如日志、指标上报),可保留同步调用,但要明确标注“调用方需保证 Update 快速返回”
观察者注册时如何防止重复添加和内存泄漏
Go 没有对象身份比较(如 Java 的 ==),直接用 == 比较接口值不可靠。重复注册会导致同一观察者收到多次通知;不清理已失效的观察者(如 HTTP handler 关闭后未解绑)会造成内存泄漏。
立即学习“go语言免费学习笔记(深入)”;
- 不依赖地址比较,改用带唯一 ID 的观察者(例如
type LoggerObserver struct { id string }),Attach前先遍历检查id - 提供
Detach(id string)显式解绑,比传入接口值更可控 - 对长生命周期的观察者(如全局 metrics collector),建议配合
context.Context实现自动注销:在Update中检测ctx.Err() != nil后主动调用Detach
type Observer interface {
Update(event interface{})
}
type Subject struct {
mu sync.RWMutex
observers map[string]Observer // 改用 map 便于按 id 查找/删除
}
func (s *Subject) Attach(id string, o Observer) {
s.mu.Lock()
defer s.mu.Unlock()
s.observers[id] = o
}
func (s *Subject) Detach(id string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.observers, id)
}
func (s *Subject) Notify(event interface{}) {
s.mu.RLock()
obs := make([]Observer, 0, len(s.observers))
for _, o := range s.observers {
obs = append(obs, o)
}
s.mu.RUnlock()
for _, o := range obs {
go o.Update(event) // 异步分发
}
}
真正难处理的是跨 goroutine 生命周期管理——比如一个 HTTP handler 注册为观察者,但 handler 返回后其闭包变量仍被 subject 持有。这时候光靠 Detach 不够,得结合 context 或 weak reference 思路(如用 sync.Map 存储带弱引用标记的观察者),但 Go 标准库不支持真正的弱引用,所以最实际的做法还是靠代码规范:谁注册,谁负责在合适时机解绑。










