如何使用Golang实现观察者模式_Golang观察者模式事件通知实现

2次阅读

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

如何使用Golang实现观察者模式_Golang观察者模式事件通知实现

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 标准库不支持真正的弱引用,所以最实际的做法还是靠代码规范:谁注册,谁负责在合适时机解绑。

text=ZqhQzanResources