广播不能用简单for循环写,需为每连接配独立goroutine和send chan,读写clients需加锁,写前加超时select,断连须close send并delete map。

直接用 gorilla/websocket 的 WriteMessage 循环发给每个连接,撑不过 500 并发就会卡死或丢消息——广播不是“for range + 写”,而是要绕开阻塞、竞态和单点故障。
为什么 broadcast 循环一跑就卡住
常见错误是写个 for _, conn := range clients { conn.WriteMessage(...) },看似简单,实则三处致命:一个 client 写超时,整个循环阻塞;clients map 并发读写 panic;连接已断但没清理,WriteMessage 触发 write: broken pipe 后 panic。根本原因在于 WebSocket 连接本身不可重入、不可共享、不可批量同步写。
- 不要在主线程或 HTTP handler 里直接调用
WriteMessage - 所有写操作必须走独立 goroutine + 每连接专属
send chan []byte - 遍历
clients前必须加sync.RWMutex.RLock(),删连接时用Lock() - 每次写前加
select超时,例如case c.send ,跳过慢/挂掉的 client
Hub.run() 必须单 goroutine 消费 broadcast channel
Hub.broadcast 是一个带缓冲的 chan []byte(建议 size 256),只负责收消息;真正分发必须由唯一一个 hub.run() goroutine 完成。它不处理业务逻辑,只做三件事:从 broadcast 收包、加读锁遍历 clients、对每个 client 的 send channel 尝试非阻塞投递。这样设计能隔离失败——某个 client 写失败,不影响其他 client 接收,也不会拖垮整个 hub。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
-
broadcastchannel 缓冲区不能为 0,否则上游发消息会卡在 send 上 -
hub.run()里不要做 JSON 序列化、权限校验等耗时操作,只做转发 - 如果广播频率极高(如每秒千条),可考虑把
clients拷贝成切片再遍历,避免锁持有时间过长 - 别用
sync.Cond.Broadcast()替代,它不传数据,只唤醒,且新连接无法被通知到
连接池与写缓冲必须显式配置
默认 ReadBufferSize 和 WriteBufferSize 都是 4096 字节,小消息没问题,但一旦有 10KB 的通知或二进制帧,频繁系统调用和内存分配会吃掉大量 CPU。更关键的是,不设 WriteBufferPool,每个连接都会独占一份写缓冲,1000 连接就是 1000×64KB ≈ 64MB 内存白耗。
- 升级器初始化时显式设缓冲:
ReadBufferSize: 32768,WriteBufferSize: 65536 - 启用缓冲池:
Upgrader.WriteBufferPool = &sync.Pool{New: func() interface{} { return make([]byte, 0, 65536) }} - 连接建立后立即调用
conn.SetWriteDeadline(time.Now().Add(10 * time.Second)),防止慢连接长期占用资源 - 对大消息(>4KB)考虑分片发送,或提前压缩:
conn.EnableWriteCompression(true),但仅限文本类 payload
客户端注销必须主动 close(send) + delete(map)
很多人以为连接断开后,conn.ReadMessage 返回 io.EOF 就完事了,其实这只是读端关闭。如果没手动关 client.send channel 并从 clients map 删除,那个 send 会一直挂着,hub.run() 每次广播都尝试往它里面塞数据,直到超时跳过——这会造成内存泄漏和广播延迟升高。
- 读协程检测到
err != nil后,必须调用hub.unregister -
hub.run()收到 unregister 请求后,先delete(clients, client),再close(client.send) - 客户端 struct 里
sendchannel 必须带缓冲(如make(chan []byte, 64)),否则写协程一阻塞,整个广播就卡住 - 前端断连时,服务端可能几分钟后才收到 EOF,所以心跳检测(ping/pong)必须开启:
conn.SetPingHandler(...),超时未响应即强制注销
真正难的不是写出来,而是让广播在 2000 连接、每秒 300 条消息、30% client 网络抖动的场景下仍不丢、不卡、不 panic——这要求每个环节都放弃“大概能行”的假设,对每一个 write、每一个 map、每一个 chan 都明确它的生命周期和失败路径。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










