ReadMessage错误即断连信号,须用websocket.IsUnexpectedCloseError区分重连;WriteMessage需单goroutine串行调用;重连应独立goroutine+context控制;心跳为保活刚需,须手动设置Ping/Pong及超时。

ReadMessage 错误就是断连信号,别等超时
conn.ReadMessage() 一出错,连接基本就断了。它不会重试,也不会阻塞等待——错误返回即事实断连。常见错误包括 *websocket.CloseError、io.EOF、net.OpError(比如 dial tcp: i/o timeout)。不能只写 if err != nil { return } 就退出读循环,必须用 websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) 判断是否该重连:
– websocket.CloseNormalClosure(1000)是主动关闭,不重连
– websocket.CloseAbnormalClosure(1006)是异常断开,应立即进重连流程
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 解析失败、证书过期):
– 用 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))
– 注册 conn.SetPongHandler(),收到 Pong 后重置 SetReadDeadline
– 心跳间隔建议 30~45 秒,读取超时设为 60 秒,退避上限 30 秒,最大重试次数建议 5~10 次
– 单独为每个连接起 ticker 在万级连接时会吃光调度器资源;更优做法是集中式扫描器 + sync.Map 记录最后心跳时间
立即学习“go语言免费学习笔记(深入)”;
重连逻辑里最容易被忽略的是:重连成功后没重置读写 deadline,也没重新注册Ping/Pong handler;还有就是忘记关闭旧 connection 导致 fd 泄漏——这两个问题在线上压测时往往第二天才暴露。


















