不能直接用 gorilla/websocket 裸调 readmessage/writemessage,因其 conn 无生命周期管理与线程安全封装;需用带缓冲的 send channel 隔离并发写、register/unregister channel 串行化连接管理、服务端主动 ping 心跳并设读写超时。

为什么不能直接用 gorilla/websocket 做裸连接
裸调 conn.ReadMessage() 和 conn.WriteMessage() 看似简单,但一上线就崩:并发写 panic、心跳失效、连接泄漏、消息错乱全堆一起。根本原因是 *websocket.Conn 本身不管理生命周期,也不做线程安全封装——它只是个“协议管道”,所有状态、并发、超时、清理都得你补。
Client 结构体必须带 send channel 而不是直接暴露 conn
每个连接必须绑定专属写通道,否则多个 goroutine 同时调 conn.WriteMessage() 会触发 concurrent write to websocket connection panic,或把 A 的消息发给 B。
-
send通道要带缓冲(比如make(chan []byte, 256)),避免广播时卡住 hub -
writePump里必须用select+default或带超时的send,防止客户端不消费导致 channel 堵死 - 发送前检查
conn.CloseChan()是否已关闭,避免往已关连接写引发 panic - 别用
sync.Mutex包WriteMessage——锁太粗,高并发下成瓶颈
hub 必须单 goroutine 处理注册/注销,禁用原生 map
用 map[*Client]bool 存连接?只要一个 goroutine 在遍历广播、另一个在删连接,立刻 fatal error: concurrent map read and map write。这不是概率问题,是必现 crash。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 必须通过
register和unregisterchannel 把增删操作收口到唯一 goroutine - 广播时遍历的是本地 slice 副本(从 map 拷贝),不是原 map,读写彻底隔离
-
sync.Map不推荐:它不支持遍历,广播仍需额外维护活跃连接列表 - 注销逻辑不能写在
readPump的defer里——错误退出时可能漏 unregister
心跳必须服务端主动发 Ping,且配 SetReadDeadline
只等客户端 ping?Nginx 默认 proxy_read_timeout 60s,静默断连后服务端还当它在线,消息全积压,直到下次 WriteMessage() 才报错——此时已晚。
- 在
writePump中启time.Ticker定期发websocket.PingMessage - 调
conn.SetPingHandler(conn.PongHandler()),别自己写空函数覆盖默认行为 - 在
readPump开头设conn.SetReadDeadline(time.Now().Add(30 * time.Second)),超时即视为离线 - 收到
io.EOF或websocket.IsCloseError(err)要主动unregister并close(conn.CloseChan())
真实项目里最常被跳过的点是:注册/注销没走 channel 统一入口,或者心跳只靠客户端发起。这两个漏洞一旦上线,连接数上来就断连率飙升,日志里全是 write deadline exceeded 却查不到根因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










