必须调用cancel(),因其负责关闭done通道、停止timer并解除父子引用;不调用会导致timer goroutine持续驻留、内存泄漏及gc无法回收。

context.WithTimeout 不是“设个时间就完事”,它背后启动的定时器 goroutine 必须手动清理,否则会泄漏。
为什么 context.WithTimeout 会泄漏 goroutine
context.WithTimeout 内部用 *time.Timer 实现倒计时,启动一个独立 goroutine 等待超时触发 Done() 关闭。这个 goroutine 不会因业务提前结束而自动退出——它只认 cancel() 调用或时间到。
- 高频创建(如每个 HTTP 请求都调一次)会导致 timer goroutine 堆积
- 未调
cancel()时,timer 持有对子 context 的引用,阻碍 GC 回收 - 即使你 10ms 就返回了,那个 goroutine 还得傻等满 5s 才停
defer cancel() 放错位置等于没写
常见错误是把 defer cancel() 写在 goroutine 启动前,比如:
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
go func() {
// 这里可能已经跑完了,但 cancel 还没被调
doSomething(ctx)
}()
defer cancel() // 错!handler 还没退出,goroutine 却可能早结束了
正确做法取决于任务类型:
- 一次性任务:在启动 goroutine 后、等待结果前
defer cancel() - 长期 worker:goroutine 内部必须监听
ctx.Done(),并在退出前调cancel() - HTTP handler 场景:直接在 handler 函数开头 defer,因为 handler 退出 = 请求生命周期结束
别用 context.Background() 套 WithTimeout
直接基于 context.Background() 创建超时 context,会切断与客户端连接状态的联动:
- 用户关闭页面,
r.Context()已取消,但你的WithTimeout还在倒计时 - 父 context 因子 context 未 cancel 而无法被回收,双重泄漏
- 正确姿势是:
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
QueryContext 失败后不检查 err 就等于没用
数据库驱动(MySQL ≥ v1.6.0,PostgreSQL ≥ v1.12.0)支持 cancel 语义,但前提是:
- 必须用
db.QueryContext(ctx, ...),不是db.Query(...) - 必须判断
err == context.Canceled或err == context.DeadlineExceeded - 失败时
rows可能为nil,直接rows.Close()会 panic
最常被忽略的点是:cancel 函数本身不释放底层连接,只有 QueryContext 主动中断并返回错误后,连接才归还连接池。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











