必须用 sync.map + rwmutex 保护订阅列表,因 chan 是点对点通信原语,无法广播;sync.map 不保护 value 并发安全,需结构体内嵌 rwmutex 管理切片,且发布须非阻塞发送并及时清理断连订阅者。

直接用 sync.Map + 带缓冲 chan 就能跑起来,但必须配 RWMutex 保护订阅列表,否则并发删订阅时 panic。
为什么不能只用一个全局 channel 做发布
Go 的 chan 是点对点通信原语:一次 send 只能被一个 recv 消费。如果让 10 个 goroutine 同时 range 同一个 chan string,结果只有 1 个能收到消息,其余全部卡死在读操作上——这不是广播,是竞态抢答。
- 常见错误现象:
for range ch在多个协程里监听同一通道,每条消息只进一个协程 - 真实场景需求:WebSocket 聊天室推消息给所有在线用户,不是“谁抢到算谁的”
- 根本限制:
chan没有“复制分发”能力,必须靠代码层补足发布-订阅语义
如何用 sync.Map + RWMutex 安全管理订阅者
sync.Map 只保证 key 级原子性,不保护 value(比如 []chan interface{})的并发读写。所以不能直接存切片,得包一层结构体,内部用 RWMutex 控制。
- 推荐结构:
sync.Map存topic → *subList,其中*subList内部用sync.RWMutex保护[]chan interface{} - 订阅时调用
subList.add(ch),发布前先RLock()快照遍历,避免遍历时切片被修改导致 panic - 每个订阅通道必须带缓冲(如
make(chan interface{}, 16)),否则一个慢消费者会拖垮整个Publish - 发布逻辑必须用
select { case ch 非阻塞发送,跳过写不进的通道,防止卡死
Subscribe 和 Unsubscribe 的安全写法
客户端断开后若没及时清理其 chan 和 map 条目,会导致内存缓慢上涨、广播变慢,甚至触发 panic: send on closed channel。
-
Subscribe(ctx context.Context, topic string)应返回一个取消函数,负责删 map 条目 + 关闭chan - 接收循环必须检查
conn.WriteMessage()返回值;若为websocket.ErrCloseSent或io.EOF,立刻从 map 中移除该连接 - 别把
http.Request.Context()直接传给长期运行的订阅逻辑——它的生命周期太短,容易误关 - 注销时要原子查找 + 删除,不能直接操作切片;建议用
sync.Map.Load/Store/Delete配合结构体内锁
最易被忽略的是发布时的非阻塞投递和订阅者的资源清理时机——慢消费者不处理、断连不注销,模块跑几天后就会开始丢消息或卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











