不能直接用 channel 当事件队列,高并发下易卡死或 panic;应使用 sync.Map 存储带缓冲的通道切片,Publish 非阻塞,显式关闭每个 chan。

别用 channel 直接当事件队列,高并发下容易卡死或 panic;真正能扛住每秒数百次发布、数十个订阅者的轻量方案,核心就三点:用 sync.Map 存通道切片、Publish 必须非阻塞、每个 chan 必须带缓冲并显式关闭。
为什么不能直接用 map[string][]chan interface{}
自己加 sync.RWMutex 保护的普通 map,在并发 Subscribe 和 Publish 时极易触发 panic —— 尤其是遍历 range subs[topic] 过程中,另一个 goroutine 正在 delete(subs, topic),Go 运行时会直接崩溃。这不是偶发,是确定性 panic。而 sync.Map 的 Range 是快照语义,发布时只通知“当时已注册”的订阅者,既安全又符合预期。
-
sync.Map的LoadOrStore和Load是唯一合法操作方式,map[key] = val会编译失败 - value 类型必须是
[]chan Event(不是map[string]chan Event),否则一个 topic 只能有一个订阅者 - 如果 topic 数量极少(比如就 3–5 个),用全局
sync.RWMutex + map更直观,不必硬套sync.Map
Publish 必须用 select { case ch
同步发送 ch 是最常见错误:只要某个订阅者的 chan 满了、或消费 goroutine 卡在 DB 查询上,整个 Publish 就阻塞,HTTP handler 直接超时。这在上线后第一波流量里就会暴露。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 缓冲大小建议设为
16或32:太小(如 1)导致频繁丢消息,太大(如 1024)占用内存且延迟不可控 -
default分支不能空着,至少打日志或上报 metric,否则线上积压完全无感知 - 不要在
select里加time.After:那不是防阻塞,是掩盖问题;真正的背压要靠上游限流或下游扩容
Unsubscribe 不关 channel 就等于埋 goroutine 泄漏
只从 sync.Map 里删掉 chan 引用,却不调用 close(ch),会导致消费端一直阻塞在 for range ch 上。反复订阅退订几次后,runtime.NumGoroutine() 就会持续上涨,最后 OOM。
- 正确流程:先
close(ch),再从sync.Map中移除对应切片项(注意是重建切片,不是原地删) - 消费者必须写成
for msg := range ch形式,这样close(ch)后循环自动退出 - 别在
Subscribe里直接启动 goroutine 处理消息——生命周期难控制,cancel 信号容易丢失
最易被忽略的一点:事件类型别用裸 interface{}。传进来的 msg.(*UserCreated) 一错就是 panic,IDE 和 go vet 完全无法检查。要么定义泛型事件 type Event[T any],要么统一用带 Payload() T 方法的接口,把类型约束收在编译期。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










