Go中不应使用Observer接口,因其隐式实现导致运行时panic、Detach失效及强依赖;应改用func(Event)函数类型配合sync.Map与struct{} token实现安全轻量的观察者模式。

Go 里没有 Observer 接口,硬套 Java/C# 的 Register/Notify + 接口继承写法,八成会出竞态、panic 或内存泄漏——不是模式本身有问题,是 Go 的类型系统和并发模型不买账。
为什么别用 Observer 接口定义观察者
Go 的接口是隐式实现,Update(event interface{}) 这类签名看似统一,实则埋了三颗雷:
- 调用方无法静态检查传入对象是否真能响应事件,运行时才 panic:nil pointer 或类型断言失败
- 注册时用
==比较两个匿名函数永远为 false,Detach彻底失效 - 接口让被观察者强依赖具体类型,失去运行时按需增删、条件过滤的能力
真正轻量且安全的做法,是把观察者视为可执行单元:func(Event)。它自带类型约束,编译期报错,注册/注销靠显式 token 控制,不靠值比较。
用 sync.Map + 显式 token 管理生命周期
sync.Map 适合读多写少的场景(比如配置监听、状态变更通知),但要注意它不保证遍历顺序,也不支持原子性批量删除。关键点在 token 设计:
立即学习“go语言免费学习笔记(深入)”;
- 别用
uintptr存函数地址——闭包每次调用地址可能不同,同名方法在不同实例中无法区分 - 推荐用
struct{}作 token,由调用方自己持有并传回注销:unsubscribe := s.Subscribe(handler) -
Subscribe返回注销函数,而非布尔值或 error,语义清晰,不易漏调
示例核心逻辑:
type Event struct { Type string Data map[string]interface{} }
type Subject struct { handlers sync.Map }
func (s *Subject) Subscribe(handler func(Event)) func() {
token := struct{}{}
s.handlers.Store(token, handler)
return func() { s.handlers.Delete(token) }
}
func (s *Subject) Notify(e Event) {
s.handlers.Range(func(_, v interface{}) bool {
if fn, ok := v.(func(Event)); ok { fn(e) }
return true
})
}切片方案更直观,但必须做快照复制
如果事件频率不高、观察者数量稳定([]func(Event) + sync.RWMutex 更易读易测。但直接遍历原切片会 panic,必须复制一份快照:
- 注册/注销走
Lock(),只锁毫秒级;通知走RLock()+append(observers[:0:0], observers...)复制 - 别在
Notify中启动 goroutine 调用 handler——同步语义应由使用者决定;若需异步,显式写go handler(e) - 传参用值类型小结构体(如
UserCreatedEvent)比interface{}安全,避免全项目 grep 类型断言
event 名称和结构体设计影响长期可维护性
初期图省事传 map[string]interface{} 或 json.RawMessage,后期加字段、改语义、补日志时会疯掉。真实项目里,值得花两分钟定义明确的事件结构体:
- 字段首字母大写,确保外部可访问
- 用过去式命名(
UserDeletedEvent),表达“已发生”语义 - 避免嵌套过深的 interface{} 层级,类型断言超过两层就该重构
- 如果真要支持多类型事件,用
switch e := event.(type)分支处理,别搞反射注册
最常被忽略的点:注销时机。goroutine 已退出但 handler 仍挂在 sync.Map 或切片里,下次 Notify 就 panic。token 必须由注册方保管,并在明确生命周期终点(如 HTTP handler 返回、DB transaction 结束)调用注销函数。


















