gin本身不内置websocket支持,必须依赖gorilla/websocket或gin-gonic/websocket完成连接升级;关键在于正确管理连接生命周期(及时close、避免goroutine泄漏)和消息路由(并发安全连接池、错误时清理)。

单机跑通一个能收发消息的 WebSocket IM,关键不是堆功能,而是守住连接生命周期和消息路由两个核心点。Gin 本身不内置 WebSocket 支持,必须靠 gorilla/websocket 或 gin-gonic/websocket 做升级,选哪个、怎么管连接、怎么避免 goroutine 泄漏,才是实际开发中最常卡住的地方。
用 gin-gonic/websocket 还是 gorilla/websocket?
官方推荐用 gin-gonic/websocket,它只是对 gorilla/websocket 的轻量封装,API 更贴合 Gin 风格,但底层完全一致。别被名字误导——它不是 Gin 官方维护的库,只是社区维护的适配包。
-
gin-gonic/websocket的Upgrade函数直接接收*gin.Context,写法更简洁;gorilla/websocket则需手动从c.Writer和c.Request提取 - 两者都要求设置
CheckOrigin,生产环境不能写死return true,否则任何域名都能连你的 ws 接口 - 如果你后续要对接 Redis Pub/Sub 或做连接迁移(比如灰度下线某台机器),
gorilla/websocket的 Conn 类型更通用,生态工具链更成熟
upgrader.Upgrade 后必须立刻处理读写循环
很多新手在 Upgrade 成功后,只写了个 conn.WriteMessage 就返回,结果前端连上就断。WebSocket 连接一旦升级成功,HTTP 生命周期就结束了,后续所有通信必须由你手动维持。
- 必须启动一个 goroutine 处理
conn.ReadMessage,否则连接会因超时被服务端关闭 - 必须在
ReadMessage出错(比如客户端断开)时,显式调用conn.Close()并从连接池中删除该 Conn,否则内存持续增长 - 不要在读循环里直接调用
conn.WriteMessage回复——高并发下容易阻塞,应改用带缓冲的 channel 或独立写协程
如何安全地保存和广播 WebSocket 连接
全局 map 存 *websocket.Conn 是最简方案,但有严重隐患:map 非并发安全,且无法按用户 ID 或房间 ID 快速索引。
- 用
sync.Map替代普通 map,避免读写冲突;但注意sync.Map的Load/Store不能替代锁逻辑 - 连接键建议用业务标识,比如
user_id:123或room:public,而不是 Conn 指针本身——指针无法序列化,也不便于跨进程管理 - 广播前先检查
conn.WriteMessage返回值,遇到websocket: close sent或i/o timeout要立即清理该连接 - 如果用 Redis + Pub/Sub 做消息中转(比如多实例部署),连接池必须只存本机活跃 Conn,跨机广播走 Redis,别混用
真正难的不是写通第一条消息,而是让连接在用户切页、网络抖动、服务重启时不丢状态、不堆积 goroutine。连接池的清理时机、心跳超时判定、消息重投机制——这些细节没压测过,上线后就会暴露。别急着加群聊或离线消息,先把单聊的连接存活率做到 99.9% 再往下走。











