Go 语言原生不支持 WebSocket,必须依赖 gorilla/websocket 等第三方库;因 net/http 仅支持 HTTP 底层,无法处理 WebSocket 帧格式、掩码规则、ping/pong 心跳等核心逻辑,自行实现极易出错。

Go 语言原生不支持 WebSocket,必须依赖第三方库;gorilla/websocket 是当前最稳定、文档最全、社区最活跃的选择,其他库(如 gobwas/ws)在连接复用、并发压测或错误恢复上容易出问题。
为什么不用 net/http 直接升级 WebSocket 连接
net/http 只提供底层 HTTP 协议支持,WebSocket 握手虽基于 HTTP Upgrade,但后续帧格式(掩码、opcode、长度编码、ping/pong 处理)完全独立。自己解析 Sec-WebSocket-Accept 并实现帧读写极易出错——比如忽略客户端必须掩码、服务端不应掩码的规则,导致 Chrome 拒绝接收数据。
- 浏览器强制要求客户端发帧时设置掩码位,服务端若也掩码,Chrome 会静默丢弃该帧
-
http.ResponseWriter的Write不保证原子性,多 goroutine 写同一连接可能产生帧粘连 - 缺少内置 ping/pong 心跳逻辑,连接空闲超时后 TCP 连接仍存在,但 WebSocket 已失效
gorilla/websocket 最小可用服务端写法
关键不是“怎么启动”,而是“怎么避免 panic 和资源泄漏”。以下是最简但生产可用的结构:
func serveWs(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return // 不要 log.Fatal,否则整个 HTTP server 挂掉
}
defer conn.Close() // 必须 defer,否则连接未关闭就返回,fd 耗尽
// 启动读协程:只负责读消息,不做业务处理
go func() {
defer conn.Close()
for {
_, _, err := conn.ReadMessage()
if err != nil {
return // 如 io.EOF 或 websocket.CloseMessage,直接退出读循环
}
}
}()
// 主协程:负责写(例如广播、定时推送)
for range time.Tick(10 * time.Second) {
if err := conn.WriteMessage(websocket.TextMessage, []byte("tick")); err != nil {
return
}
}}
立即学习“go语言免费学习笔记(深入)”;
-
upgrader必须全局复用,不能每次请求 new 一个,否则配置(如 CheckOrigin)丢失 -
conn.ReadMessage()会阻塞,必须放在单独 goroutine,否则写操作会被卡住 -
conn.WriteMessage()不是线程安全的,多个 goroutine 同时写需加锁或用conn.WriteJSON()配合 channel 串行化
客户端连接失败常见原因和调试方法
浏览器控制台报 WebSocket connection to 'ws://...' failed,90% 和服务端握手响应有关:
- 检查
upgrader.CheckOrigin是否返回true:默认拒绝所有跨域,本地前端用file://或不同端口都会被拦 - 确认 URL 协议是
ws://(开发)或wss://(生产),不是http://—— 浏览器不会自动降级 - 用
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" http://localhost:8080/ws手动触发握手,看是否返回101 Switching Protocols - 如果用 Nginx 反向代理,必须显式透传 Upgrade 头:
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";
WebSocket 不是“开个连接一直用到底”的银弹。连接断开不可控(网络抖动、NAT 超时、客户端休眠),真正难的是重连策略、消息去重、离线缓存——这些没法靠一个 Upgrader 解决,得结合业务状态机设计。










