defer cancel() 永远不执行,因为长连接的 handler 通过 for-select 循环阻塞运行、永不返回,导致 defer 延迟语句无法触发;必须在 conn.read() 返回 io.eof 或网络错误时显式调用 cancel(),才能释放资源、避免 goroutine 泄漏。

长连接 handler 里 defer cancel() 为什么永远不执行
因为 HTTP handler 函数返回才触发 defer,但长连接(如 WebSocket、自定义 TCP 协议)的主循环通常用 for { select { ... } } 阻塞运行,永不返回。只要连接没断,defer cancel() 就卡在函数入口,根本不会跑。
常见错误写法:
func handleConn(w http.ResponseWriter, r *http.Request) {<br>
ctx, cancel := context.WithCancel(r.Context())<br>
defer cancel() // ← 这行永远不会执行<br>
serveLoop(ctx, conn)<br>
}
- 只要
serveLoop没退出,整个 handler 就卡住,defer不触发 -
ctx被传进循环,但cancel()没绑定到真实断连事件,等于没信号 - 后果:goroutine 泄漏、TLS session 不释放、DB 连接池耗尽、内存持续增长
必须把 cancel() 绑定到 conn.Read() 返回 io.EOF 或网络错误
连接断开时,conn.Read() 会返回 io.EOF、net.ErrClosed 或其他底层错误(如 read: connection reset by peer),这才是真实的“断连信号”。只有在这里调 cancel(),才能让所有阻塞在 ctx.Done() 上的 goroutine 立刻退出。
正确模式:
func serveLoop(ctx context.Context, conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
log.Println("client disconnected")
cancel() // ← 在这里显式调用
return
}
log.Printf("read error: %v", err)
return
}
// 处理数据...
select {
case
- 不要只靠
conn.SetReadDeadline();它只能防卡死,不能替代语义化取消 - 每次
Read()后必须检查err,不能忽略或只用if err != nil { continue } - 如果用了
io.Copy(),需包装成带ctx的版本,或改用io.CopyN()+ 循环检查
资源清理必须穿透到每个阻塞点,不能只靠 GC
GC 不会帮你关 TCP 连接、释放 TLS handshake 状态、归还预分配 buffer、关闭 *sql.Conn。这些都得在 ctx.Done() 分支里手动做。
- DB 连接:若用
db.Conn(ctx)获取了*sql.Conn,必须显式调.Close() - TLS session:断连后应丢弃整个
tls.Conn实例,或清理conn.ConnectionState().PeerCertificates - 自定义 buffer pool:在
select { case 中归还 - 后台 ticker:用
time.Ticker的必须defer ticker.Stop(),否则泄露 goroutine
多个 goroutine 共享一个 cancel() 时的坑
cancel() 是幂等的,但重复调用会掩盖逻辑错误——比如本该只关一次的 DB 连接被关了两次,可能引发 panic 或静默失败。
- 不要在每个 goroutine 里都写
defer cancel() - 推荐做法:只在一个地方(通常是读循环末尾或
ctx.Done()监听 goroutine)调一次cancel() - 用
sync.Once包一层更保险:once.Do(func() { cancel() }) - 监听
ctx.Done()的 goroutine 应该是“清理总控”,负责统一关闭 DB、Redis client、ticker 等
真正难的不是写 cancel(),而是确保它在连接断开那一刻被调用,且所有依赖它的子 goroutine 都能立刻响应。任何一层漏掉,资源就滞留——这不是 GC 的问题,是你没给它回收的时机。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











