WebSocket连接断开时ReadMessage和WriteMessage不会panic,而是返回非nil error;需检查err!=nil、设WriteDeadline、主动ping探测并指数退避重连,配合消息ID与幂等机制保障数据一致。

WebSocket连接断开时,ReadMessage 和 WriteMessage 会直接 panic 吗?
不会 panic,但会返回非 nil 的 error —— 这是绝大多数人误判重连时机的根源。比如 conn.ReadMessage() 在 TCP 连接已关闭时返回 "use of closed network connection",而 conn.WriteMessage() 可能卡在缓冲区、超时后才报 "i/o timeout" 或 "broken pipe"。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有
ReadMessage/WriteMessage调用必须检查err != nil,不能只看err == io.EOF - 不要依赖
conn.IsClosed()(它只是个状态标记,不反映真实连接) - 对
WriteMessage建议配合SetWriteDeadline,否则可能阻塞 goroutine - 常见错误现象:程序没报错但消息发不出去,其实是写操作 hang 在了内核发送缓冲区
重连逻辑放在 ReadMessage 报错后,还是另起 goroutine 定期 ping?
两个都得做,但角色不同:ReadMessage 报错是「被动发现」,ping 是「主动探测」。只靠前者,服务端静默断连(如 NAT 超时、负载均衡踢掉空闲连接)时客户端会长时间无感知。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须启用
conn.SetPingHandler并在 handler 里发Pong,否则服务端可能因收不到响应而断连 - 客户端自己要起一个 goroutine 每 20–30 秒调用一次
conn.WriteMessage(websocket.PingMessage, nil) - ping 失败(write error 或超时)应立即触发重连,别等下一次 read
- 避免在
ReadMessage错误处理里直接重连——此时可能还在读缓冲区残留数据,需先conn.Close()
net.Dial 重连失败后,指数退避该从多少秒开始?
从 1 秒开始,上限设到 60 秒足够。太激进(如 100ms 起步)容易打爆服务端连接数;太保守(如固定 5 秒)又拖慢恢复速度。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每次失败后乘以 1.6~2.0(不是固定 +1),例如:1s → 2s → 4s → 8s → 16s → 32s → 60s(封顶)
- 重连前加 jitter(随机偏移 ±10%),防止多个客户端同步重连造成雪崩
- 记录重连次数和间隔,当连续 5 次失败且间隔已达 60 秒,可考虑降级为轮询 HTTP 接口
- 注意
context.WithTimeout传给websocket.DefaultDialer.Dial,否则 DNS 解析卡住会拖慢整个退避节奏
重连成功后,如何安全恢复未发送完的消息队列?
不能简单把旧队列全重发——服务端可能已处理过部分消息,重复投递会破坏业务语义。关键在于「按序、去重、可追溯」。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每条业务消息带唯一
id(如 UUID 或单调递增序列号),服务端做幂等判断 - 客户端维护一个内存队列,
WriteMessage成功后才从队列 pop;失败则保留并标记pending - 重连成功后,先发一条
sync_request消息,让服务端返回最近 N 条已接收消息 ID,再比对本地 pending 队列决定哪些要重发 - 避免在重连 goroutine 里直接循环发消息——要用带限速的 sender,否则可能瞬间冲垮新连接
最麻烦的其实不是重连本身,而是怎么让业务层不感知连接抖动。消息 ID、服务端幂等、客户端 pending 状态管理,这三块漏掉任何一环,都会让“看似连上了”变成“数据乱了”。










