每个websocket连接必须配至少两个goroutine:一个专读、一个专写,混用会panic或丢消息;因为*websocket.conn非并发安全,同一goroutine中交替readmessage和writemessage虽不直接panic,但若其他goroutine同时写就会触发concurrent write panic,且阻塞式写会导致心跳、读取停摆,最终被nginx静默断连。

每个 WebSocket 连接必须配至少两个 goroutine:一个专读、一个专写,混用会 panic 或丢消息。
为什么不能在一个 goroutine 里 ReadMessage + WriteMessage
gorilla/websocket 的 *websocket.Conn 不允许并发调用 WriteMessage —— 即使是同一 goroutine 中交替调用,只要中间有其他 goroutine 同时触发写(比如后台广播),就会触发 concurrent write to websocket connection panic。
更隐蔽的问题是:如果在读循环里直接 conn.WriteMessage() 推送,一旦网络卡住或客户端不收,WriteMessage 会阻塞整个 goroutine,导致后续心跳、Ping、甚至 ReadMessage 都停摆,连接在 60 秒内被 Nginx 静默断开。
- 读写必须分离:readPump 只负责
ReadMessage和分发;writePump 只从 channel 拿数据、调WriteMessage - writePump 必须是独立 goroutine,且生命周期绑定连接(defer 中
ticker.Stop()、conn.Close()) - 别在 readPump 里做业务逻辑(如 DB 查询、HTTP 调用),它卡住 = 整条连接失活
怎么安全地往连接发推送消息
所有写请求必须串行化,推荐用带缓冲的 channel + 单 writer goroutine:
给每个连接分配 send chan []byte(例如 make(chan []byte, 64)),所有推送方(HTTP handler、事件监听器、定时任务)都往这个 channel 发消息;writePump goroutine 循环 并调 <code>conn.WriteMessage()。
- 写前检查
conn.IsClosed()或用select带default防 channel 堵死 - 每次
WriteMessage前必须设conn.SetWriteDeadline(time.Now().Add(writeWait)),建议10 * time.Second - 捕获
io.EOF、websocket.IsCloseError(err)、net.OpError,出错立即break并关闭连接 - 别用
sync.Mutex包WriteMessage—— 锁住写会拖慢所有用户,channel 才是 Go 式解法
goroutine 泄漏比内存泄漏更危险
一个连接典型对应 3 个 goroutine:readPump、writePump、ping ticker。它们不会随连接关闭自动退出,除非你显式清理。
常见泄漏点:
- 没在
defer里ticker.Stop(),连接关了但 ticker 还在发WriteControl→ panic - readPump 中
ReadMessage返回io.EOF后没break,继续循环 → goroutine 卡在select或下一次ReadMessage - 没监听
conn.UnderlyingConn().(net.Conn).SetReadDeadline级别的错误,误判为“还在连” - 用
map存连接但删的时候没加锁,导致遍历广播时 panic,整个 hub goroutine 崩溃
最稳妥做法:所有连接管理走单 hub goroutine + register/unregister channel,增删都在一个地方做,彻底避开并发 map 读写和 goroutine 清理竞态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











