长连接 handler 必须将 context.Context 作为第一参数传入,否则 ctx.Done() 无法中断阻塞 I/O,导致协程卡死;HTTP 用 r.Context()、gRPC stream 用 stream.Context();子 goroutine 需传递 ctx 并显式释放资源;阻塞读写必须用 select + ctx.Done()。

长连接 handler 必须把 context.Context 当第一参数传入
不这么做,ctx.Done() 就进不到真正阻塞的 I/O 里,协程会卡死成僵尸。HTTP handler 用 r.Context(),别自己 new context.Background();gRPC stream 直接用 stream.Context(),它已集成客户端断连信号。
常见错误:func handleConn(conn net.Conn) { ... } —— 完全脱离 context 控制;正确写法是 func handleConn(ctx context.Context, conn net.Conn) { ... },且所有子 goroutine 都要接收并传递这个 ctx。
注意:子 goroutine 里不能只监听 ctx.Done() 就 return,还得显式关闭 conn、释放 buffer pool、cancel db transaction 等——GC 不管这些。
阻塞读写必须用 select + ctx.Done(),不能裸调 Read/Write
conn.Read() 或 stream.Recv() 是阻塞调用,不加 context 监听,cancel 信号永远收不到。写成 for { n, _ := conn.Read(buf) } 就等于放弃控制权。
立即学习“go语言免费学习笔记(深入)”;
必须配合 select:
select {
case n, err := conn.Read(buf):
// 处理数据
case <-ctx.Done():
// 清理资源,return
}更稳妥的做法是结合 conn.SetReadDeadline() 和 ctx.Deadline(),避免 Read 卡在内核态(尤其在反向代理后);对 http.ResponseWriter 写流也一样,每次 Flush() 前检查 ctx.Err() != nil。
gorilla/websocket 的 Ping/Pong 必须手动启用
默认不发 Ping,也不自动响应 Pong。SetPongHandler(nil) 不等于保活,只是关掉默认处理;你得自己启一个 goroutine 定期调 conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeWait))。
关键点:
-
SetPongHandler要重置conn.SetReadDeadline(),否则一次 Pong 后超时不会更新 -
pingPeriod应设为pongWait * 0.9,比如pongWait = 55 * time.Second,那 ping 就得 ≤ 49s 发一次 -
writeWait单独设(推荐 10s),防止WriteMessage卡死拖垮整个 goroutine - 所有超时值必须严格小于 Nginx 的
proxy_read_timeout(常见 60s),否则中间设备先断,服务端还在等
连接管理 map 并发写 panic 是高频翻车点
直接 map[string]*websocket.Conn 或 map[uint64]*net.Conn 并发读写必 panic:“concurrent map read and map write”。
安全做法:
- 用
sync.RWMutex包一层,读多写少时开销极小 - key 用
uint64(如原子自增 ID),别用 string,省哈希和 GC 压力 - value 里嵌
sync.Once和atomic.Bool,防多个 goroutine 同时触发清理逻辑 - 连接关闭时,必须从 map 删除 + 关闭
conn+ cancel 子ctx,三者缺一不可
最易被忽略的是:连接异常关闭(比如 io.EOF、websocket.CloseAbnormalClosure)时,reader/writer goroutine 若没覆盖这些 error 分支,就会永远卡在 ReadMessage() 或 select 上,状态显示 [IO wait],但实际已不可回收。


















