gin 官方不提供 websocket 实现,必须搭配 gorilla/websocket;调用 upgrader.upgrade 会 panic 或 400 是因中间件干扰响应头或连接已被写入,必须在原始 responsewriter 和 http.request 上直接调用,且升级后所有通信只能通过 websocket.conn 进行,严禁再调用 c.json 等方法。

直接用 github.com/gin-gonic/websocket 不行——这个包根本不存在,Gin 官方不提供 WebSocket 实现,必须搭配 gorilla/websocket 才能跑通。
为什么 Gin 路由里调用 upgrader.Upgrade 会 panic 或 400?
核心原因是 HTTP 协议升级(upgrade)对请求头、响应状态、连接生命周期有严格要求,Gin 的中间件链和默认 Writer 会干扰底层连接接管。
-
upgrader.Upgrade必须在 Gin 的 原始 ResponseWriter 和 *http.Request 上调用,不能经过任何中间件(比如 Logger、Recovery),否则 Header 已被写入或连接已关闭 - 务必在
GET路由中处理,且不能返回任何 JSON/HTML —— 升级成功后,该连接就脱离 HTTP 生命周期,后续所有读写都走*websocket.Conn - 常见错误现象:
http: response.WriteHeader on hijacked connection或websocket: bad handshake,基本都是因为用了c.JSON、c.String等方法,或者在Upgrade前写了响应 - 正确姿势:路由 handler 内第一行就调用
upgrader.Upgrade,失败立即 return,成功后立刻进入for循环读写,不要调用任何c.*方法
CheckOrigin 默认拒绝跨域,但生产环境不能设为 return true
前端页面域名和后端 API 域名不一致时,浏览器会先发 OPTIONS 预检,再发 Upgrade 请求;CheckOrigin 是校验 Origin 头的钩子,不处理好会导致连接被静默拒绝。
- 开发阶段可临时设为
func(r *http.Request) bool { return true },但上线前必须收紧 - 推荐写法是白名单匹配:
strings.Contains(r.Header.Get("Origin"), "your-domain.com"),注意 Origin 可能带http://或https:// - 如果用 Nginx 反向代理,确保
proxy_set_header Origin $scheme://$host;透传 Origin,否则后端收不到原始值 - 别忽略 HTTPS 场景:WSS 连接下,浏览器强制校验 Origin,且只接受
https://开头的白名单
连接断开后不清理 map[*websocket.Conn]bool 会导致内存泄漏
WebSocket 连接是长生命周期对象,Go runtime 不会自动回收已断开但未显式删除的 *websocket.Conn 指针,尤其当用 map 缓存连接做广播时,泄漏速度与在线用户数正相关。
- 必须在
ReadMessage或WriteMessage报错退出循环时,同步从全局 map 中delete(connections, conn) - 不能只依赖
defer conn.Close()—— 它只关底层 TCP 连接,不触发 map 清理逻辑 - 容易踩的坑:把
conn作为 map key,但没处理conn.RemoteAddr()变化(比如 NAT 后多客户端共 IP),导致多个连接覆盖同一个 key - 更健壮的做法是用自增 ID 或 UUID 作 key,
conn存 value,并在conn.Close()后主动删 entry
真正麻烦的不是建立连接,而是连接生命周期管理:什么时候 close、谁负责 close、close 后资源是否释放干净。这些细节不写进 handler 主循环里,光靠 defer 或全局变量根本兜不住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











