Go中不用接口定义Observer易出错,因缺乏静态检查导致运行时panic;应改用func(event)类型存储回调,配合sync.RWMutex保护增删、快照复制实现无锁遍历,避免nil指针或静默忽略。
为什么 Go 里不用接口定义 Observer 就容易出错
go 没有类继承,观察者模式不能靠“实现 observer 接口”来约束行为。如果只靠约定函数签名(比如 update()),调用方根本没法静态检查对象是否真的能被观察。结果就是运行时 panic: nil pointer dereference 或者静默忽略通知。
真正可行的做法是让被观察者(Subject)只持有 func() 或带参数的闭包,由注册方自行封装逻辑。这样既类型安全,又避免接口膨胀。
- 注册时传入一个
func(event interface{}),而不是自定义结构体 - 被观察者内部用
[]func(interface{})存储回调,不是[]Observer - 不要为“观察者”单独建
type Observer interface{ Update(...)—— 它在 Go 里是累赘
如何安全地管理 observer 列表的增删和并发调用
多个 goroutine 同时调用 Register() 和 Notify() 时,切片追加和遍历会引发 panic 或漏通知。但直接上 sync.RWMutex 又可能因读多写少场景造成锁争用。
更轻量的做法是:写操作(Register/Unregister)加锁,读操作(Notify 中的遍历)用快照——每次通知前复制当前列表。这样读完全无锁,写只锁毫秒级时间。
-
Unregister不要边遍历边删,而是用map[uintptr]struct{}标记待移除项,再在下次快照时过滤 - 快照复制用
append(observers[:0:0], observers...)避免底层数组复用 - 如果 observer 执行可能阻塞,
Notify中应启动 goroutine 分发,但需考虑调用方是否期望同步语义
Notify 时怎么传参才不踩内存或生命周期坑
传指针还是值?传 struct 还是 interface{}?关键看事件数据是否会被 observer 异步持有。如果 Notify(&data) 后 data 在栈上就回收了,异步 goroutine 里解引用就是未定义行为。
稳妥做法是:所有通知参数走 interface{},且要求调用方确保其底层值逃逸到堆上(比如用 &MyEvent{...} 或构造后直接传)。被观察者不负责深拷贝——那会掩盖设计问题。
- 避免传局部变量地址:
Notify(&localVar)是高危操作 - 如果事件结构固定,可定义
type EventData struct{ ... },然后统一传EventData值类型(小结构体)或*EventData(大结构体或需修改) - 不要在
Notify内部做 JSON marshal/unmarshal——那是业务逻辑,不属于模式本身
一个够用的 minimal 实现长什么样
下面这段代码去掉注释不到 30 行,覆盖注册、通知、安全删除,且无外部依赖:
type Subject struct {
mu sync.RWMutex
observers []func(interface{})
}
func (s *Subject) Register(f func(interface{})) {
s.mu.Lock()
defer s.mu.Unlock()
s.observers = append(s.observers, f)
}
func (s *Subject) Notify(event interface{}) {
s.mu.RLock()
obs := make([]func(interface{}), len(s.observers))
copy(obs, s.observers)
s.mu.RUnlock()
for _, f := range obs {
f(event)
}
}
它没做 Unregister,因为真实场景中,如果你需要动态注销,大概率得配合 context 或弱引用机制——而 Go 的 runtime 不支持弱引用,所以多数时候用 channel + select 控制生命周期更实际。这点常被教程忽略。


















