不能直接用 gin.context 做 websocket 升级,因为 gin 不内置 websocket 支持,其 writer 是单次写入、不可升级的 http 响应流,复用会导致“response wrote after hijack”错误或 panic;必须通过 http.hijacker 劫持底层 tcp 连接,交由 gorilla/websocket 管理帧收发。

为什么不能直接用 gin.Context 做 WebSocket 升级
因为 Gin 本身不内置 WebSocket 支持,gin.Context 的 Writer 是单次写入、不可升级的 HTTP 响应流。直接调用 c.Writer.Write() 或试图复用它处理长连接,会导致 http: response wrote after hijack 错误或 panic。
必须用标准库的 http.Hijacker 接口把底层 TCP 连接“劫持”出来,再交给 gorilla/websocket(目前最稳定)来管理帧收发。
- 别在路由 handler 里调用
c.Abort()后还操作c.Writer - 升级前确保响应头未写出:不能有
c.JSON()、c.String()等提前写响应的操作 -
gorilla/websocket的Upgrader需显式设置CheckOrigin,否则跨域时会返回 403
如何安全地把 Gin 路由和 WebSocket 升级逻辑组合起来
核心是让 Gin 处理路径匹配和中间件(如鉴权),但把连接升级交给标准 http.HandlerFunc 执行。Gin 的 gin.WrapH() 是关键桥梁。
示例:用户携带 token 参数接入,需先校验再升级
// 先定义标准 http handler
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, // 生产环境应校验 origin
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
// 这里可调用你的 JWT 解析函数
if !isValidToken(token) {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return // 错误已由 Upgrade 内部处理
}
defer conn.Close()
// 后续消息读写逻辑...
}
- 用
router.GET("/ws", gin.WrapH(http.HandlerFunc(wsHandler)))注册路由 - 不要在
wsHandler里调用任何gin.Context方法(如c.Param()),它已脱离 Gin 生命周期 - 鉴权失败必须用
http.Error(),不能靠 Gin 的c.AbortWithStatusJSON()
如何避免 WebSocket 连接泄漏和 goroutine 泄漏
每个连接至少启动两个 goroutine:一个读、一个写。如果只读不写或只写不读,conn.ReadMessage() 或 conn.WriteMessage() 会永久阻塞,导致 goroutine 积压。
必须配对使用 context.WithCancel 控制生命周期,并监听连接关闭信号:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
defer cancel()
for {
_, _, err := conn.ReadMessage()
if err != nil {
if !websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Println("read error:", err)
}
return
}
}
}()
go func() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case
- 所有
conn.ReadMessage()和conn.WriteMessage()前必须设 deadline,否则网络中断时 goroutine 不会退出 - 不要用
time.AfterFunc做心跳,它无法被取消;必须用ticker+select+ctx.Done() - 全局连接池建议用
sync.Map存*websocket.Conn,键用用户 ID,避免 map 并发写 panic
如何设计消息广播但不阻塞单个慢连接
直接遍历所有 *websocket.Conn 并同步调用 WriteMessage() 会导致快连接等慢连接,严重拖慢整体吞吐。必须引入写队列和独立写 goroutine。
典型做法:每个连接绑定一个带缓冲 channel(如 chan []byte),写 goroutine 从 channel 拉消息并发送;广播时向所有 channel 发送副本。
- channel 缓冲大小建议设为 32~64,太小易丢包,太大占内存
- 发送前检查 channel 是否已满(用
select+default),满则conn.Close() - 广播消息时,用
sync.Pool复用[]byte序列化结果,避免高频 GC - 别用
json.Marshal在广播循环里反复调用——提前序列化好再发
真正麻烦的不是代码怎么写,而是连接断开时如何清理 channel、停止 goroutine、从连接池中移除条目。漏掉任意一环,内存和 goroutine 就会缓慢增长。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











