因为Go的channel是“消费即移除”模型:一个msg被某个goroutine接收后即从channel中移除,其他goroutine无法再接收到该消息,故无法实现向多个goroutine广播。

为什么不能直接用 chan 向多个 goroutine 广播?
因为 Go 的 channel 是“消费即移除”模型:一个 msg 被某个 goroutine 从 读走,其他 goroutine 就再也收不到了。这不是广播,是竞态分发。
- 常见错误现象:
for range ch启动两个协程监听同一个 channel,结果只有其中一个收到每条消息 - 真实需求场景:WebSocket 聊天室里一条消息要推给所有在线用户,不是只推给某一个
- 根本原因:channel 不是发布-订阅机制,它没有“复制分发”能力,必须靠代码层补足
- 性能影响:强行复用同一 channel 做“伪广播”,会导致消息丢失、逻辑错乱,调试时难以定位——你以为发了,其实只进了一个协程
怎么用 sync.Map + chan 安全管理多客户端?
每个 WebSocket 连接需要独立的写通道(send chan []byte),而所有连接的注册/注销必须线程安全;sync.Map 比 map + sync.RWMutex 更适合高频增删的在线列表。
- 实操建议:定义
var clients sync.Map // key: *websocket.Conn, value: *client,其中*client包含send chan []byte - 注册时调用
clients.Store(conn, c),断开时clients.Delete(conn),无需手动加锁 - 广播循环中遍历:用
clients.Range(func(key, value interface{}) bool { ... }),避免迭代时 map 被修改 panic - 注意:不要把
send通道设为无缓冲——否则某个卡住的客户端会让整个广播协程阻塞在c.send
Redis PubSub 只负责“传出去”,不负责“连谁”
单机广播能跑通,但一上集群就丢消息——因为每个服务实例只维护本地 clients,彼此不知道对方连了谁。Redis PubSub 是解法之一,但只是半截链条。
- 关键判断:Redis 解决的是「跨进程通知」,不是「连接状态同步」;你仍需在每个节点上维护自己的
clients映射 - 实操要点:
pubsub := client.Subscribe(ctx, "ws:global:broadcast")后,收到消息只能遍历本机clients发送,不能查 Redis 里有谁在线 - 容易踩的坑:
conn.WriteMessage()不能在Subscribe的 goroutine 里直调——必须通过每个连接自己的sendchannel 中转,否则并发写会 panic - 务必加 context 超时:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second),否则网络抖动可能让client.Publish卡死整个 goroutine
广播协程里别用 select { case ch 随便丢消息
非阻塞发送看似稳妥,但实际掩盖了连接异常——比如客户端已断开,但 send channel 还没来得及关闭,消息被静默丢弃,用户收不到任何提示。
- 正确做法:广播时对每个
c.send执行带超时的 select,并检查是否已关闭:if _, ok := - 更稳的结构:每个连接配一个
writePump协程,监听自己的sendchannel;广播协程只负责往这些 channel 写,不碰连接本身 - 兼容性提醒:前端 WebSocket 断连后,Go 服务端不会立刻感知,得靠心跳(
conn.SetPingHandler)或读操作返回io.EOF才能清理 - 真正难的不是发消息,是确认“谁还在听”——这个状态永远只能本地维护,Redis 存不了实时连接句柄
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











