WebSocket连接需设写入截止时间并配对心跳处理器,写操作应加锁且用writeJSONSafe封装;广播应采用单协程+channel模式,每个client独享send channel,断连时关闭对应channel。

WebSocket 连接建立后怎么不丢消息
客户端连上来就发了一条欢迎消息,但很多人发现 conn.WriteMessage 直接 panic 或静默失败——根本原因是没检查连接状态,也没做写入超时控制。
-
conn.SetWriteDeadline必须在每次写之前调用,WebSocket 不是 TCP 长连接那种“写就完事”,底层可能卡在缓冲区或网络抖动中 - 如果用
gorilla/websocket,conn.SetPingHandler和conn.SetPongHandler要配对设好,否则心跳失败后连接会被静默关闭,后续广播就漏掉这个 client - 写操作建议包一层
writeJSONSafe函数:先加锁(避免并发写 panic),再设 deadline,再defer conn.SetWriteDeadline(time.Time{})清掉,不然下次写会沿用过期时间
广播消息时怎么避免 goroutine 泄漏
常见写法是每来一条消息,就起一个 go func() 遍历所有 clients 并发推送——结果是用户一多,goroutine 数暴涨,GC 压力大,还容易触发 runtime: goroutine stack exceeds 1000000000-byte limit。
- 广播不该用“为每个 client 起 goroutine”,而应复用一个后台协程 + channel:所有待广播的
message统一发到broadcastchannel,由单个for range broadcast协程串行分发 - 每个 client 连接要带自己的
sendchannel,广播协程往每个client.send发消息;client 的读写协程负责从send取、调WriteMessage - 务必在 client 断开时 close 其
sendchannel,并在 send 协程里用select { case msg, ok := 检查,否则会卡死在无缓冲 channel 上
怎么安全地管理在线用户列表
直接用 map[*websocket.Conn]bool 存 client?看似简单,但 map 并发读写 panic 是高频错误;换成 sync.Map 又容易误用 LoadOrStore 导致重复注册或漏删。
- 推荐用
sync.RWMutex包一层普通 map,key 用自定义 ID(比如uuid.NewString()),而不是 *websocket.Conn——指针可能被 GC 或复用,不可靠 - 注册时机必须在 WebSocket 握手完成、且首次
ReadMessage成功之后(比如收到登录 payload),不能在Upgrader.Upgrade返回后立刻塞进 map - 删除动作不能只靠 defer 或 defer close,得在读协程 detect 到
io.EOF或websocket.CloseMessage后显式调mutex.Lock() → delete() → mutex.Unlock(),否则广播时会向已断开连接写,触发 write error
为什么本地测试正常,上线后频繁断连
不是代码问题,大概率是反向代理(Nginx / ALB)或云服务商的空闲超时策略在 kill 连接。现象是前端隔几十秒收不到 pong,然后触发 onclose。
立即学习“go语言免费学习笔记(深入)”;
- Nginx 默认
proxy_read_timeout 60,必须显式设成 0(禁用)或大于你的 ping 间隔;同时配proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade"; - Cloudflare 默认强制 100s 超时,且不转发 ping/pong,得关掉「WebSockets」开关或换用其他 CDN
- Go 服务端自己也要设心跳:
conn.SetPingPeriod(25 * time.Second),并确保pingHandler里只做conn.SetWriteDeadline+conn.WriteMessage(websocket.PongMessage, nil),别加日志或 DB 查询
真正难的不是写通广播逻辑,而是让每个连接在各种中间件、超时、GC、panic 场景下都能干净退出、不拖累全局。尤其是 send channel 的生命周期和 mutex 的粒度,稍不注意就会变成半夜告警的根源。


















