不能在http handler里直接调writemessage()广播,因*websocket.conn非并发安全,多goroutine调用必panic;且网络异常时会无限阻塞,拖垮整个流程。

单机环境下用 gorilla/websocket 做广播,必须走 Hub + broadcast channel 模式;直接在 handler 里遍历 conn.WriteMessage() 会 panic 或阻塞,不是“写法问题”,而是并发模型和协议层限制决定的。
为什么不能在 HTTP handler 里直接调 WriteMessage() 广播?
WebSocket 连接对象 *websocket.Conn 的 WriteMessage() 方法不是并发安全的。两个 goroutine 同时调用它,必然触发 panic;更隐蔽的问题是:当 TCP 连接已断但 FIN 包未收到时,WriteMessage() 会无限阻塞,拖垮整个广播循环。
- 每个连接必须配独立的
writePumpgoroutine,只由它调用WriteMessage() - 必须设置
conn.SetWriteDeadline(),超时就退出,避免僵死 - 每次写完要检查 error:遇到
websocket.ErrCloseSent或use of closed network connection,就得关掉该连接的sendchannel,并通知 hub 注销
hub.broadcast 通道为什么必须带缓冲?
不带缓冲的 chan []byte 在没有 receiver 时会阻塞 sender,而 sender(通常是 readPump)一旦卡住,整个连接就读不下去了。设成带缓冲(比如 128),能让消息先排队,给 hub.run() 一个消费窗口。
- 缓冲大小不是越大越好:太大可能积压旧消息,掩盖连接异常
- 典型值取 64~256,取决于平均消息吞吐和容忍延迟
- 如果广播频繁且消息体大,要考虑
[]byte复制开销,可改用sync.Pool复用字节切片
集群环境下 hub.broadcast 为什么失效?
因为 hub.broadcast 是进程内 channel,只在当前 Go 实例里流转。A 实例收到消息,往自己的 broadcast 一塞,B 实例根本看不到——它的 hub 是另一个内存地址,map 和 channel 全都不共享。
- 跨实例广播必须把“发消息”这一步换成网络操作,比如 Redis Pub/Sub
- 每个实例仍保留本地
hub和clients map,只是不再往本地broadcast写,而是client.Publish(ctx, "ws:global:broadcast", msg) - 所有实例都
Subscribe同一个频道,在独立 goroutine 里接收并推给本机连接 - “发给某人”这种单点推送,需要额外查路由表(比如 user_id → instance_id),不能靠内存 map 解决
真正容易被忽略的点是:hub.run() 的 for-select 循环不只是为了“转发”,它是唯一能原子化处理注册、注销、广播三件事的地方。任何绕过这个循环的直连操作,都会导致状态不一致——比如连接已断,但还在 clients map 里,下次广播就会 panic。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











