不调用 cancel() 会导致定时器 goroutine 泄漏;context.WithTimeout 启动的 goroutine 会持续运行至超时,即使业务已结束,且高频使用时引发 goroutine 和 timer 堆积、阻碍 GC。

不调用 cancel() 会导致定时器 goroutine 泄漏
Go 的 context.WithTimeout 内部会启动一个独立的 goroutine,用来在超时时间到达时关闭 ctx.Done()。这个 goroutine 由 *timerCtx 管理,它持有一个 *time.Timer 实例,并通过 channel 发送信号。
如果忘记调用返回的 cancel() 函数,即使业务逻辑早已结束,该定时器也不会被停止,goroutine 会一直运行到超时触发——哪怕你只用了 10ms 就返回了结果,它仍要等满整个 timeout 时间(比如 5s)才退出。
-
timerCtx不会自动感知“子任务已完成”,它只认cancel()或超时 - 高频创建 context(如每 HTTP 请求都
WithTimeout)时,泄漏的 goroutine 和 timer 会快速堆积 - 泄漏的 timer 还会阻止关联的
*time.Timer被 GC,因为其内部有未释放的系统资源引用
defer cancel() 必须在 goroutine 启动之后、退出之前调用
常见错误是把 defer cancel() 放在 goroutine 启动前,比如:
cancel()
- 若子 goroutine 是“一次性任务”,应在内部监听
ctx.Done()并自然退出,外层在wait后再调用cancel() - 若子 goroutine 是长期 worker(如轮询),必须在
select中显式处理,并在退出前释放资源 - 绝不要在 goroutine 外部用
defer cancel()试图“保底”——它和 goroutine 生命周期不匹配
HTTP handler 中基于 r.Context() 派生 timeout context 才能避免双重泄漏
直接用 context.Background() 调 WithTimeout 是危险的:它切断了与客户端连接状态的联动。用户关闭页面后,你的 timeout context 仍会继续倒计时,同时父 context(r.Context())也因子 context 未 cancel 而无法被回收。
标准库的 http.Request.Context() 本身就是一个可取消 context——当客户端断连,它会自动触发取消。你应该在此基础上叠加超时:
func handler(w http.ResponseWriter, r *http.Request) {
// ✅ 正确:继承请求生命周期,再加业务超时
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel() // 这里 defer 是安全的:handler 函数退出 = 请求生命周期结束
<pre class="brush:php;toolbar:false;">req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
// ...
}
- 这样,cancel() 触发时,既停掉自己的 timer,也向父 context 表明“我已退出”,帮助
r.Context()及时释放 - 若 handler 提前 return(比如参数校验失败),
defer cancel()仍会执行,timer 被及时清理 - 注意:
r.Context()本身不需要你手动 cancel,它是由 net/http server 管理的
数据库查询中 QueryContext 调用后仍需检查 rows.Next() 期间的 ctx.Err()
很多开发者以为调用 db.QueryContext(ctx, ...) 就万事大吉,其实不然。驱动层只保证“发起查询”阶段响应 cancel;但若查询返回了大量行,遍历 rows.Next() 时仍可能卡住(比如网络抖动、慢解析)。
此时 ctx.Done() 已关闭,但 rows.Next() 不会自动中断——它只是个普通循环控制。
- 必须在每次
rows.Next()后检查if ctx.Err() != nil { return } - 不能只在
QueryContext返回 err 时判断,因为成功返回 rows 后,ctx 仍可能中途超时 -
rows.Close()不会触发 cancel,它只释放本地内存,不中断远端查询执行
真正的资源回收依赖两层:驱动响应 ctx.Done() 中断查询执行 + 应用层主动检查上下文状态退出遍历循环。漏掉任何一层,都可能导致 goroutine 和连接句柄滞留。











