长连接 handler 中 defer cancel() 无效,因 handler 永不返回,cancel 不会执行;必须将 cancel 绑定到真实断连事件(如 conn.read() 返回 io.eof),并确保 ctx 透传至所有阻塞点(如 select { case

长连接关闭后,资源不释放、goroutine 不退出,不是 GC 没干活,而是你没给它“回收信号”——context.Context 必须穿透到阻塞 I/O 层,且 cancel() 必须在连接断开时显式调用。
为什么 defer cancel() 在长连接 handler 里不管用
HTTP handler 函数返回才触发 defer,但长连接(如 WebSocket、gRPC stream)的 handler 通常永不返回。若只在入口处 ctx, cancel := context.WithCancel(r.Context()) 然后 defer cancel(),cancel 永远不会执行。
- 错误模式:
func handleWS(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithCancel(r.Context()); defer cancel(); serveLoop(ctx, conn) }——serveLoop卡住,defer永不触发 - 正确做法:把
cancel()绑定到连接生命周期事件,例如在conn.Close()后立即调用,或监听ctx.Done()的 goroutine 中统一清理 - 注意:不要在多个 goroutine 里重复调
cancel(),它幂等,但掩盖逻辑错误(比如本该只关一次的 DB 连接被关了两次)
阻塞读写必须用 select + ctx.Done(),不能只靠 conn.SetReadDeadline()
conn.SetReadDeadline() 可防卡死,但无法替代 context 监听:超时只是时间维度控制,而 ctx.Done() 是语义化取消(如客户端主动断连、服务端主动驱逐、配置热更新下线)。
- 错误写法:
for { conn.SetReadDeadline(time.Now().Add(30 * time.Second)); n, _ := conn.Read(buf) }—— 客户端断开后,Read()可能返回io.EOF或net.ErrClosed,但你不检查就继续循环,资源滞留 - 正确写法:
select { case ,且每次读完立刻检查 <code>err是否为io.EOF或网络错误 - gRPC stream 同理:必须用
stream.Recv()配合select,不能裸调用;stream.Context()已集成断连信号,别自己 wrap 新 context
数据库连接、TLS session、自定义 buffer pool 怎么清理
GC 不会帮你关 TCP 连接、释放 TLS handshake 状态、归还预分配内存块——这些都得在 ctx.Done() 分支里手动做。
- DB 连接:若用
sql.DB,它自带连接池,但长连接中持有的*sql.Conn(从db.Conn(ctx)获取)必须显式调.Close() - TLS session:若复用
tls.Conn并启用了 session resumption,断连时应调conn.ConnectionState().PeerCertificates清理缓存,或直接丢弃整个tls.Conn实例 - buffer pool:若用
sync.Pool分配读写缓冲区,需在ctx.Done()分支里将所有已借出的[]byte归还,否则 pool 会持续持有内存不释放 - 切记:所有清理动作必须在同一个 goroutine 内完成,避免并发读写共享状态(如 map、slice)引发 panic
代理层断连检测有延迟,ctx.Err() 不是唯一依据
Nginx、Envoy、iOS 网络栈可能缓存 FIN 包数秒,导致 ctx.Err() 滞后返回 context.Canceled。单靠它判断连接已死,容易造成资源空转。
- 必须组合检测:每次 I/O 前检查
ctx.Err() != nil,同时对conn.Read()/stream.Recv()的返回值做完整错误分类 - 关键错误要立即退出:如
io.EOF、net.ErrClosed、rpc.ErrShutdown、transport.StreamError.Code == codes.Canceled - 心跳保活失败也要触发 cancel:若应用层心跳超时(如 3 次未收到 pong),应主动调
cancel(),而不是等底层 TCP timeout
真正难的不是写几行 cancel(),而是在每个 goroutine 入口都传入同一份 context,并确保所有阻塞点都响应它——漏掉一个 select,就多一个僵尸协程;晚调一次 cancel,就多一份泄漏资源。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











