必须将重连逻辑抽离读循环,用独立goroutine+context控制;ReadMessage错误即断连信号,需用websocket.IsUnexpectedCloseError区分重连;WriteMessage须串行化;心跳为保活刚需,重连后须重设Ping/Pong及超时。

Go 语言里 WebSocket 断线重连不能靠“重试 ReadMessage”来实现,必须把重连逻辑从读循环里抽出来,否则会卡死、漏错、无法响应取消信号。
ReadMessage 错误就是断连信号,别等超时
gorilla/websocket 的 conn.ReadMessage() 一返回非 nil error,连接基本就断了。它不会重试,也不会阻塞等待恢复——错误即事实断连。常见错误包括:*websocket.CloseError(含 Code 字段)、io.EOF、net.OpError(如 dial tcp: i/o timeout)。
不能只写 if err != nil { return } 就退出读循环,必须用 websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) 判断是否该重连:
-
websocket.CloseNormalClosure(1000)是主动关闭,不重连 -
websocket.CloseAbnormalClosure(1006)是异常断开,应立即进重连流程 - 其他如
net.ErrClosed、io.ErrUnexpectedEOF也属于需重连的异常
WriteMessage 并发不安全,必须串行化写
conn.WriteMessage() 不是并发安全的。多个 goroutine 同时调用,轻则消息乱序,重则 panic。
生产环境唯一靠谱方案是起一个专属写 goroutine,监听专用 channel:
- 所有业务逻辑往
writeCh chan []byte发消息 - 写 goroutine 从 channel 取、统一调
conn.WriteMessage() - 重连成功后,必须关闭旧
writeCh并新建一个,否则往已关闭的 channel send 会触发send on closed channel - 别用
sync.Mutex包裹写操作——高吞吐下成瓶颈,且重连后锁状态难清理
重连必须独立 goroutine + context 控制
在 ReadMessage 出错后直接写 for { dial(); time.Sleep() } 是典型反模式:它卡死当前 goroutine,无法响应外部停止指令,还掩盖真实错误(比如 DNS 解析失败、证书过期)。
正确做法:
- 把重连抽成独立 goroutine,用
context.WithCancel(parentCtx)创建子 ctx - 重连 goroutine 监听
ctx.Done(),收到信号即退出 - 每次重连前先
if conn != nil { conn.Close() },否则旧连接 fd 不释放,Linux 下很快 hit ulimit - 重连成功后,重新启动读/写 goroutine,并调用
conn.SetPingHandler()和conn.SetWriteDeadline()
心跳不是可选项,而是保活刚需
NAT 中间件(家用路由器、云 LB)通常 60s 左右静默 kill 空闲连接。gorilla/websocket 默认配置没设 KeepAlive 和 WriteDeadline,必须手动补全:
- 启动一个 goroutine 定期调
conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second))(建议间隔 ≤30s) - 注册
conn.SetPongHandler(),收到 Pong 后重置SetReadDeadline(例如设为time.Now().Add(60 * time.Second)) - 服务端若未及时响应 Ping,客户端会因读超时断连;客户端若未发 Pong,服务端也应主动关连接
最易被忽略的一点:重连后必须重新设置心跳 handler 和 deadline——旧连接的 timer 和 handler 不会自动迁移,不重设等于裸奔。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











