gorilla/websocket 是最稳选择,因其RFC 6455兼容完整、边界处理扎实,正确实现握手、帧解析、ping/pong、分片重组与关闭码协商,且经多年生产验证。

直接用 gorilla/websocket,别自己造轮子,也别轻易换库。它已稳定支撑生产环境多年,RFC 6455 兼容性完整,边界处理扎实,是当前 Go 生态中 WebSocket 服务的事实标准。
为什么 gorilla/websocket 是最稳选择
Go 标准库 net/http 不实现 WebSocket 协议,硬写握手、帧解析、ping/pong、分片重组、关闭码协商极易出错。而 gorilla/websocket 正确处理了所有这些细节:比如自动响应 Ping 帧、按规范生成 Pong、校验掩码(客户端必须掩码)、拒绝超长控制帧、防止缓冲区溢出 panic。相比之下,nhooyr.io/websocket 默认开启严格 origin 检查和 TLS 验证,开发调试门槛高;gobwas/ws 更轻但缺少连接生命周期管理工具,容易漏掉 Close 或 SetReadDeadline 导致 fd 泄漏。
常见踩坑点:
-
http: invalid Upgrade request:中间件或 handler 提前调用了r.Body.Read()或r.ParseForm(),消耗了 request body,导致 upgrade header 丢失 -
http: response.WriteHeader on hijacked connection:试图用http.ResponseWriter直接写 WebSocket 帧,必须通过upgrader.Upgrade()获取*websocket.Conn
ReadMessage 和 WriteMessage 必须分离 goroutine
*websocket.Conn 的读写方法不是并发安全的。多个 goroutine 同时调 conn.ReadMessage() 或 conn.WriteMessage() 会 panic,报 concurrent write to websocket connection 或读取错乱。
立即学习“go语言免费学习笔记(深入)”;
正确做法是为每个连接启动两个 goroutine:
- 一个专读:阻塞调用
conn.ReadMessage(),收到消息后投递到广播 channel 或业务逻辑 - 一个专写:从 channel(如
broadcast)收消息,串行调conn.WriteMessage()
写 goroutine 还需注意:WriteMessage 可能因对端断连或网络卡顿而阻塞,务必配 conn.SetWriteDeadline(),并在 recover 中主动 close 连接。
大消息、空闲连接、内存暴涨的三重防
真实场景下,用户可能发 20MB 的 base64 图片、客户端假死不发 ping、恶意构造超长文本帧——这些都会让服务迅速崩溃。
必须显式设置:
-
conn.SetReadDeadline(time.Now().Add(10 * time.Second)):每次ReadMessage前都设,避免 goroutine 卡死 -
conn.SetReadLimit(1024 * 1024):限制单帧最大字节数,超限自动关闭连接 -
upgrader.CheckOrigin = func(r *http.Request) bool { return r.Host == "myapp.com" }:上线前禁用return true,防止被滥用于扫描或 DDoS
若需处理真正的大帧(如流式二进制数据),应改用 conn.NextReader() + io.CopyN 分块读,而不是依赖 ReadMessage 一次性加载。
连接管理与广播不能只靠 map
用 map[*websocket.Conn]bool 存客户端看似简单,但存在竞态:goroutine A 刚删完 conn,goroutine B 就在遍历 map 并调 conn.WriteMessage(),触发 panic。
必须加锁或换结构:
- 用
sync.RWMutex包裹 map 读写 - 更推荐用
sync.Map+ channel 广播:新连接注册进sync.Map,广播消息统一发到全局broadcast chan []byte,每个写 goroutine 自行判断本连接是否还存活 - 每个连接需绑定
context.WithCancel,在defer conn.Close()里调 cancel,方便上层统一中断
真正容易被忽略的是心跳和连接老化:光靠 TCP keepalive 不够,NAT/代理常在 5 分钟后静默断开连接。必须实现应用层 Ping/Pong,且 SetPingHandler 要返回 nil 表示接受,否则默认会关连接。



















