Observer模式解耦关键在于发布方不感知订阅方类型、生命周期和执行方式;需定义轻量Event接口,订阅返回取消函数并先close再删除。

Go 语言里 observer 模式真正解耦的关键,不是“注册接口”,而是让发布方完全不感知订阅方的类型、生命周期和执行方式。
用最小契约接口替代 interface{}
直接用 interface{} 当事件参数,等于把类型检查推到运行时——每次 event.(*UserCreatedEvent) 都可能 panic。实际项目里最常崩在这儿。
- 定义统一的轻量接口:
type Event interface { Topic() string; Timestamp() int64 } - 所有具体事件(如
UserCreatedEvent、OrderPaidEvent)都实现它 - 发布方只依赖
Event,新增事件类型时Publisher.Notify()完全不用改 - 订阅方收到后,可安全地用类型断言或 switch type 判断具体类型,但 panic 风险已收口到自己代码里
订阅必须返回取消函数,不能只删引用
常见错误是 Unsubscribe(topic, ch) 只从 sync.Map 里删掉 ch,但没关 channel。结果发布方还在往已无人接收的 ch 里写,要么阻塞,要么 panic(尤其带缓冲的 ch 满了之后)。
-
Subscribe()应返回func():先close(ch),再从sync.Map中Delete() - 发布方推送前用
select { case ch 非阻塞发送,避免被慢订阅者拖垮 - 测试 goroutine 泄漏?用
runtime.NumGoroutine()在反复 Subscribe/Unsubscribe 后比对数量
Context 要下沉到单次订阅,不是全局开关
给整个 Publisher 加一个 context.Context,一取消就全停——这不符合真实场景。UI 模块退出、HTTP 请求结束,往往只需要退订某几个 topic,而不是停掉整个事件系统。
立即学习“go语言免费学习笔记(深入)”;
Subscribe(topic string, ch chan 接收 context- 取消函数内部调用
ctx.Done()触发清理,比如关闭 channel 或标记状态 - 别在发送路径上做
select { case ——开销大,还掩盖了 channel 是否 ready 的事实
真正难解耦的点不在内存内通知,而在跨服务事件(比如发 Kafka)。那部分没法靠 sync.Map 或 chan 解决,得靠消息队列的 offset 和 ack 机制。内存内的 observer,只管好自己那一层的松耦合就行。


















