直接传回调函数破坏解耦,因发布者需感知订阅者并承担调用失败、panic、阻塞等责任;正确方式是用带缓冲的channel+goroutine分离控制流,发布者只投递,不关心接收与执行。

func(event interface{}) 不该直接用作异步通知入口,它看着解耦,实则把发布者拖进回调执行泥潭里。
为什么直接传回调函数会破坏解耦
把 func(Event) 存进 map 然后 go callback(event),看似异步,但问题藏在细节里:
- 没缓冲的 goroutine 一旦回调里有
time.Sleep或网络请求,就卡住,积压越来越多 - 某个回调 panic 会直接 kill 掉这个 goroutine,没人知道它挂了,也没法重试或告警
- 发布者要等所有回调注册完才能发事件,注册逻辑若涉及 DB 查询或网络,就变成同步依赖
- 无法限制并发数,100 个订阅者同时执行,可能打爆下游服务或耗尽 goroutine 栈
用 channel 替代函数指针做订阅入口
订阅者不该暴露执行逻辑,只该提供一个接收通道。发布者只管投递,不关心谁收、怎么收:
- 订阅时返回
chan Event,不是func(Event) - 每个订阅者自己启动消费 goroutine:
go func() { for e := range ch { handle(e) } }() - 通道必须带缓冲(如
make(chan Event, 64)),否则一个慢消费者会让所有后续事件阻塞 - 发布者调用
select { case ch ,确保永不阻塞
如何安全支持动态订阅/退订
不能一边遍历 map 里的 chan,一边删元素——会 panic。正确做法是用命令通道隔离管理逻辑:
立即学习“go语言免费学习笔记(深入)”;
- 定义两个命令类型:
subscribeCmd{topic string, ch chan}和unsubscribeCmd{topic string, ch chan} - 维护一个管理通道:
ctrlCh := make(chan interface{}, 100) - 单独起一个 goroutine 持续
for cmd := range ctrlCh,只在这里读写map[string][]chan - 发布事件时只用
RWMutex.RLock()读 map,完全避开锁竞争
别忽略订阅者的生命周期管理
没人关 channel,它就一直占内存;没人读,缓冲区就一直涨。真实系统里必须显式清理:
- 订阅时返回
CancelFunc,内部往ctrlCh发unsubscribeCmd - 消费 goroutine 用
for e := range ch,channel 关闭后自动退出 - 管理 goroutine 在广播前加
select { case 避免向已关通道写入 panic - 监控
len(ch),超过阈值就告警——这不是配置问题,是消费者处理能力出问题了
真正麻烦的从来不是“怎么发消息”,而是“怎么确认消息被稳稳接住又不拖垮系统”。


















