gorilla/websocket 是 Golang 生产环境最稳的 WebSocket 实现,需正确配置 CheckOrigin、用 channel 解耦广播、map 加读写锁管理连接、心跳检测清理断连、atomic 生成 ID,并通过二级结构支持定向推送。

gorilla/websocket 是当前 Golang 生产环境中最稳的 WebSocket 实现,直接用它就行。别折腾标准库或自己解析握手,99% 的连接失败和 panic 都源于绕开它。
gorilla/websocket 升级失败:Connection closed before receiving a handshake response
这不是协议问题,是跨域校验拦住了。
-
upgrader.CheckOrigin默认拒绝所有非同源请求,浏览器发起new WebSocket("ws://localhost:8080/ws")时根本进不了握手阶段 - 开发阶段可临时放开:
CheckOrigin: func(r *http.Request) bool { return true } - 上线前必须白名单校验,例如:
return r.Header.Get("Origin") == "https://myapp.com"或更宽松的strings.HasSuffix(r.Header.Get("Origin"), ".yourdomain.com") - 若前端走 Nginx,确认配置了
proxy_set_header Origin $http_origin;,否则Origin头被丢弃,校验永远失败 - 前端 URL 必须是
ws://或wss://,写成http://浏览器压根不发 Upgrade 请求
广播消息时程序 panic 或卡死
根本原因是直接在 HTTP handler 或 goroutine 里遍历 *websocket.Conn 调用 WriteMessage()。
-
*websocket.Conn不是并发安全的,多个 goroutine 同时写会 crash - 某个客户端网络卡顿或已断开,
WriteMessage()会阻塞当前 goroutine,整个广播流程就挂住 - HTTP handler 生命周期短,而 WebSocket 连接是长生命周期,状态无法对齐
- 正确做法:所有推送统一写入带缓冲的
broadcast chan []byte(如make(chan []byte, 100)),由独立的hub.run()goroutine 拉取并分发 - 每个连接配专属
writePumpgoroutine,监听自己的send chan []byte,调用conn.WriteMessage(),避免跨 goroutine 写
连接管理用 map 还是 sync.Map?
用原生 map 加读写锁更可控,sync.Map 并不能解决泄漏问题。
- 直接
map[*websocket.Conn]bool并发读写会 panic,必须加sync.RWMutex -
sync.Map虽线程安全,但删除不及时仍导致内存泄漏——连接断开后没从 map 清掉,*websocket.Conn对象一直被引用,GC 不回收 - 必须配合心跳检测:
conn.SetPingHandler+ 每 30s 主动WriteMessage(websocket.PingMessage, nil),收到io.EOF或websocket.CloseError就清理连接 - 连接 ID 推荐用
atomic.AddUint64(&idSeq, 1)生成,别用time.Now().UnixNano(),防重复和溢出
从 HTTP 接口触发推送(如 POST /api/push)
关键不在怎么发,而在消息是否可靠落地到在线客户端。
立即学习“go语言免费学习笔记(深入)”;
- 接口收到请求后,只做一件事:把序列化好的
[]byte发到全局broadcastchannel - 不要在接口里尝试
conn.WriteMessage(),哪怕只推给一个用户——连接可能已断、写可能阻塞、goroutine 可能泄漏 - 如果要定向推送(如推给 user_id=123),不要遍历所有连接查
user_id,改用二级结构:map[int64][]*clientConn,查完直接发到对应 client 的sendchannel - 高频小消息(如“已读回执”)建议复用
bytes.Buffer构造[]byte,避免每次json.Marshal()开销;复杂结构用conn.WriteJSON()更稳妥
Upgrade 调用解决,得靠心跳、channel 解耦、ID 绑定和明确的关闭路径。


















