必须返回带缓冲的chan而非裸回调函数,因裸回调会让发布者承担执行失败、panic、阻塞等全部责任,破坏解耦;而带缓冲chan(如make(chan event, 64))将控制权交还订阅者,发布者通过select非阻塞投递。

直接用 map[string][]chan interface{} + sync.RWMutex 就能跑通,但不加缓冲、不启消费 goroutine、不处理退订,5 分钟后就会卡死或泄漏。
为什么 Subscribe 必须返回带缓冲的 chan,而不是裸 func()
裸回调函数会让发布者承担执行失败、panic、阻塞、并发失控等全部责任——这不是解耦,是把耦合从编译期挪到运行时。而返回 chan Event 把控制权交还给订阅者:
- 缓冲大小必须显式指定,比如
make(chan Event, 64);空缓冲(0)在任意一个慢消费者下就导致整个Publish阻塞 - 发布者只做非阻塞投递:
select { case ch ,丢弃或告警,绝不等 - 订阅者自己启动消费逻辑:
go func() { for e := range ch { handle(e) } }(),生命周期清晰可控 - 不用
unsafe.Pointer比对函数地址,退订靠 channel 本身作 key,天然唯一
如何安全地 Unsubscribe 而不触发 panic 或 goroutine 泄漏
遍历 map 同时删元素会 panic;只删 map 不关 channel 会导致接收 goroutine 永久阻塞在 range ch 上。正确做法是两步闭环:
-
Unsubscribe内部先close(ch),再用sync.RWMutex安全删除 map 中对应项 - 消费 goroutine 必须用
for e := range ch,channel 关闭后自动退出,不残留 - 别依赖
defer close(ch)在 Subscribe 里关——那只是注册时关,不是退订时关 - 如果用
sync.Map,注意其Range是快照语义,退订后本次 Publish 不会通知到刚删的 subscriber,这是合理行为
Publish 为什么不能同步等待所有订阅者处理完
同步等待等于把最慢的订阅者变成系统瓶颈。真实场景中,邮件服务可能耗时 2s,风控校验要调外部 API,而积分模块只需 5ms——Publish 等它们仨一起返回,TP99 直接翻三倍。
- 正确做法是每个订阅 channel 单独起 goroutine 投递,用
sync.WaitGroup管理投递完成,但不等消费完成 - 消息体建议用不可变结构体,比如
type UserRegistered { UserID int; Time time.Time },避免多个 subscriber 并发改同一指针引发 data race - 若消息较大,传值有 GC 压力,可传
*Event但必须确保无写入;interface{} 包裹的 string/int/小 struct 可直接传 - 别在 Publish 里做深度拷贝——解耦的前提是信任订阅者自行管理消息生命周期
真正难的不是“怎么让消息发出去”,而是“怎么确认它被稳稳接住,又不拖垮别人”。缓冲大小、消费 goroutine 启动位置、channel 关闭时机——这三个点,错一个,系统就从解耦变成定时泄漏炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











