Go中WebSocket无损释放的关键是:ReadMessage返回错误后立即用websocket.IsCloseError判断,确认为1006或io.EOF时同步调conn.Close(),并确保所有相关goroutine(读、写、心跳)同步退出,避免fd和goroutine泄漏。
go 里不存在 websocket 客户端“半关闭”状态,所谓“半关闭”只是 tcp 层残留现象,不是协议层可依赖的行为;试图无损释放时,必须放弃“读停写不停”的幻想,转而以 readmessage 返回错误为唯一信号,立即关闭连接并退出 goroutine。
ReadMessage 返回 websocket.CloseAbnormalClosure(1006)意味着什么
这不是半关闭,是连接已死——浏览器关页、NAT 超时、LB 主动踢出、手机切后台,都会让底层 net.Conn 收到 RST 或 FIN,ReadMessage 立即返回 websocket.CloseAbnormalClosure(1006),伴随 read: connection closed 或 use of closed network connection。
此时:
- conn.RemoteAddr() 可能 panic,但不能靠它判断状态
- conn.WriteMessage() 几乎必然 panic,别试
- conn.UnderlyingConn() 只用于调试,生产环境别用它做逻辑分支
关键动作只有一条:websocket.IsCloseError(err, websocket.CloseAbnormalClosure) 必须是 ReadMessage 返回非 nil 错误后的第一行代码,否则漏判就等于放任 goroutine 和 fd 泄漏。
为什么不能调 conn.CloseWrite() 模拟半关闭
WebSocket 不是裸 TCP,conn.CloseWrite() 是 TCP 层语义,直接调用会破坏协议状态机:
- 客户端收不到合法
CloseMessage帧,只能等超时后报 1006 - 服务端自己下次
ReadMessage会收到read: connection closed,而非预期的关闭码 - gorilla/websocket 根本没暴露“读端关闭但写端可用”的接口,所有底层连接状态变更都会立刻传导到
ReadMessage
真正该用的是:conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(...)) —— 这才是协议级握手起点。
立即学习“go语言免费学习笔记(深入)”;
无损释放的关键动作顺序
“无损”不是指保持连接开放,而是指不泄漏资源、不 panic、不卡住 goroutine。核心动作必须严格按序执行:
- 在
ReadMessage返回 err 后,**立刻**调websocket.IsCloseError(err, ...)分类 - 若为
websocket.CloseAbnormalClosure或io.EOF,同步调conn.Close()(不是 defer) - 用
sync.Once或 channel 保证多个 goroutine(如读、写、心跳)只关一次连接 - 关闭前发
CloseMessage是可选优化,但写失败就跳过,绝不阻塞;写成功后也需conn.Close()收尾 - 所有基于该
conn的 goroutine 必须在同一时刻退出,包括心跳PingHandler—— 否则日志刷屏use of closed network connection
漏掉任意一步,fd 就会卡在 lsof -p $(pidof yourapp) 里持续增长,几十次重连后触发 dial: too many open files。
最容易被忽略的资源泄漏点
不是没写 conn.Close(),而是写了却没管住其他 goroutine:
- 读 goroutine 收到 1006 后关了连接,但心跳 goroutine 还在定时发
PingMessage - 写 goroutine 正在往 channel 塞消息,而读 goroutine 已退出,channel 无人消费,导致写 goroutine 永久阻塞
- 连接存进
sync.Map后,断连时忘了Delete,后续重连找不到旧连接,反复新建
真正的无损释放,是让整个连接生命周期里的所有协程、channel、timer、context 都同步感知并退出——conn.Close() 只是最后一环,不是第一环。


















