Go观察者模式应避免接口继承和切片管理,推荐用sync.Map+显式token实现线程安全的订阅/注销;Notify逻辑需由使用者控制异步、超时等行为,不可封装在内部。

Go 语言里没有内置的 Observer 接口或事件总线,观察者模式必须手动建模;硬套 Java/C# 那套接口继承思路,八成会写出难维护、易 panic 的代码。
为什么直接用 Observer 接口 + 切片管理观察者容易出问题
常见错误现象包括:注册后忘记注销导致 goroutine 泄漏、Update() 参数类型不统一(interface{} 还是具体结构体?)、遍历切片时并发修改引发 panic、观察者执行阻塞整个通知流程。
- 切片
[]Observer是非线程安全的,Attach/Deregister必须加锁,但锁粒度大时影响吞吐 - 用
==比较两个接口值判断是否为同一观察者不可靠——方法值每次调用可能生成不同底层指针 - 无法区分同名函数在不同上下文中的多个实例(比如两个
logger.Log实例注册了相同签名的函数) - 没有生命周期控制机制,观察者退出后回调仍挂在列表里,
Notify()时 panic
sync.Map + 显式 token 是更稳妥的注册/注销方式
核心是把“观察者”抽象为可执行函数,用唯一 token 控制其生命周期,而非依赖接口实现或地址比较。
-
token类型推荐struct{}或string(如 UUID),由调用方自己保管,注销时传回即可 - 用
sync.Map存储map[token]func(Event),读多写少场景下性能优于加锁切片 -
Subscribe()返回一个func()注销函数,使用者可主动调用,避免悬空回调 - 通知逻辑完全交给使用者决定:同步调用、
go handler(e)异步执行、加 context 超时控制都可行
示例关键片段:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
type Event struct {
Type string
Data map[string]interface{}
}
<p>type Subject struct {
mu sync.RWMutex
handlers sync.Map // map[token]func(Event)
}</p><p>func (s *Subject) Subscribe(handler func(Event)) func() {
token := struct{}{}
s.handlers.Store(token, handler)
return func() { s.handlers.Delete(token) }
}</p><p>func (s *Subject) Notify(e Event) {
s.handlers.Range(func(_, v interface{}) bool {
if fn, ok := v.(func(Event)); ok {
fn(e)
}
return true
})
}
高频增删场景下,sync.RWMutex + 普通 map 反而更清晰
当观察者注册/注销非常频繁(比如每秒数百次),sync.Map 的内部哈希分段和原子操作开销反而高于一把读写锁保护的普通 map。
- 用
map[any]func(Event)配合sync.RWMutex,写操作少时读性能几乎无损 - 键类型选
any(而非uintptr)可安全支持string、int、自定义 struct 等显式 token - 避免用
func地址作 key——闭包捕获变量后,地址不稳定,且无法区分语义相同的多个 handler - 如果需要按事件类型分发,可在
map[eventType]map[token]func(Event)上再套一层
通知逻辑必须由使用者控制,不能封装进 Notify()
这是 Go 观察者模式最容易被忽略的复杂点:是否异步、是否带超时、是否重试、错误如何处理——这些决策权必须留在业务层,而不是藏在 Notify() 内部。
- 不要在
Notify()里启动 goroutine 调用 handler,否则调用方无法感知执行结果或控制并发 - 不要在
Notify()里做类型断言或解包——event 结构应由 handler 自己解析,保持类型安全 - 如果某个 handler 可能长时间阻塞,应在注册时就明确约定:由使用者包裹
go handler(e)或加context.WithTimeout - 批量通知失败时,
Range遍历中发生 panic 会中断后续 handler 执行——需在外层加recover或改用显式 for 循环+错误收集


















