必须用gorilla/websocket而非标准库,因net/http仅支持握手升级,帧解析、心跳、连接管理等需手动实现且极易出错;gorilla封装rfc 6455全流程,提供线程安全读写、单连接专属写channel及完善错误处理。

直接说结论:用 gorilla/websocket + 读写分离 goroutine + 单连接专属写 channel 是当前最稳、最易维护的方案;别自己封装握手或复用 net/http 原生连接,那是在给自己埋雷。
为什么必须用 gorilla/websocket 而不是标准库
net/http 没有 WebSocket 支持,所谓“原生”只是能帮你做 HTTP 升级响应,但 Sec-WebSocket-Key 校验、Accept 头生成、帧解析、ping/pong 自动响应全得手写。一个拼错的 Sec-WebSocket-Accept 就会让连接静默失败,浏览器里看不到任何错误。
-
gorilla/websocket封装了 RFC 6455 全流程,Upgrader.Upgrade()一行搞定握手 - 它内置
SetPongHandler,服务端不用手动处理websocket.PingMessage - 它默认对
ReadMessage做 buffer 复用,比手写少 30% 内存分配 - 别碰已归档的
golang.org/x/net/websocket,它不维护且无并发保护
conn.WriteMessage 并发 panic 怎么避
现象:write tcp: use of closed network connection 或消息乱序,本质是多个 goroutine 同时调用同一个 *websocket.Conn.WriteMessage。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 每个连接必须绑定一个
send chan []byte(缓冲大小建议32) - 起唯一 goroutine 运行
writePump,只从该 channel 读、只调一次WriteMessage - 广播时遍历 clients map,往每个 client 的
sendchannel 发消息,而不是直接conn.WriteMessage - 发消息前加
select { case _, ok := 判断 channel 是否已关闭
连接断了但 goroutine 还卡着?超时必须设
没设 deadline 的连接,客户端拔网线后,ReadMessage 会永久阻塞,goroutine 泄漏,fd 耗尽。
- 升级成功后立刻调
conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 每次成功
ReadMessage后重置 read deadline - 在
PongHandler里只做conn.SetReadDeadline(...),别加日志或 DB 查询 -
conn.Close()必须显式调用,否则 fd 不释放,Linux 下很快 hitulimit -n
心跳保活为什么服务端要主动 ping
Nginx 默认 proxy_read_timeout 60s,只要服务端 60 秒没发数据,它就静默 kill 连接,且不通知客户端。仅靠前端定时 ping 没用。
- 服务端用
time.Ticker每 25 秒调一次conn.WriteMessage(websocket.PingMessage, nil) - 客户端收到 ping 会自动回 pong,
gorilla/websocket已注册默认PongHandler - 不要在
WriteMessage后立刻flush——它内部已做 - 如果用了 Nginx,确保配置含
proxy_http_version 1.1;和proxy_set_header Upgrade $http_upgrade;
真正容易被忽略的点是:连接池里存 *websocket.Conn 作 map key 会 panic,因为不可比较;要用 conn.RemoteAddr().String() 或自增 ID。还有,upgrader.CheckOrigin 开发期设 return true 很方便,但上线前不改成白名单,等于把实时通道裸奔暴露给全网。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










