go语言观察者模式应避免硬套接口继承,而需用sync.map+显式token管理生命周期;通知逻辑由使用者控制,支持按类型分发、条件过滤和上下文取消。

Go 语言没有内置的 Observer 接口或事件总线,观察者模式必须手动建模;直接套用 Java/C# 的接口继承思路会写出难以维护的代码。
为什么不能直接定义 Observer 接口并让结构体实现?
Go 的接口是隐式实现、鸭子类型,但观察者模式的关键不在“谁实现了接口”,而在“谁持有谁、何时通知、如何解耦”。硬套 Observer 接口容易导致:
- 通知逻辑散落在各处,
Update()方法签名难统一(比如参数是interface{}还是具体事件结构体?) - 被观察者强依赖具体观察者类型,失去运行时增删能力
- 忘记清理已失效的观察者(如 goroutine 已退出但回调仍注册着),引发 panic 或内存泄漏
map[uintptr]func(event interface{}) 不是好选择
有人用函数指针地址当 key 存 map,看似能动态注册/注销,但问题明显:
-
uintptr不是稳定标识:闭包、方法值每次调用可能生成不同地址 - 无法区分同名函数在不同上下文中的多个实例(比如两个
logger.Log实例) - 没有类型安全,
event interface{}导致下游必须做大量类型断言,易出错
更稳妥的做法是引入显式生命周期管理:用 struct{} 作 token,由注册方自己保管,注销时传回即可。
推荐实现:基于 sync.Map + 显式 Unsubscribe token
核心是把“观察者”抽象为可执行的函数,并用唯一 token 控制其生命周期。例如:
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
})
}</p>
使用时:
s := &Subject{}
unsubscribe := s.Subscribe(func(e Event) {
fmt.Println("got:", e.Type)
})
// ……
unsubscribe() // 主动注销,避免悬空回调
注意点:
- 不要在
Subscribe内部启动 goroutine 去调用 handler——通知顺序和错误处理需由使用者决定 - 如果 handler 可能阻塞,应在调用方自行加
go handler(e),而非封装进Notify -
sync.Map适合读多写少场景;若高频增删,改用sync.RWMutex+ 普通map[any]func(Event)更清晰
真实项目中往往需要事件分发粒度控制
单一 Notify(Event) 很难满足需求。常见变体:
- 按事件类型分发:
NotifyUserCreated(user User)、NotifyOrderPaid(order Order),避免所有观察者都收到无关事件 - 支持条件过滤:注册时传入
func(Event) bool,只在条件满足时触发 - 带上下文取消:
SubscribeContext(ctx context.Context, handler func(Event)),自动在ctx.Done()后注销
这些不是“模式升级”,而是对通知契约的细化——Go 里观察者是否有效,取决于你如何定义“事件”和“订阅”的语义边界,而不是接口长得像不像 UML 图。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











