必须显式调用 cancel(),否则 time.Timer 和 goroutine 不释放;超时仅关闭 Done() 和设置 Err(),timer 仍在运行,Go 不自动回收,漏调会导致内存和 goroutine 泄漏。

必须显式调用 cancel(),否则底层 time.Timer 和 goroutine 不会释放,哪怕超时已触发。
为什么 cancel() 不能省略?
超时只是关闭 ctx.Done() 并设置 ctx.Err(),但内部的 time.Timer 仍在运行。Go 不会自动回收它——没调 cancel(),timer 就一直占着内存和 goroutine。
常见错误写法:ctx, _ := context.WithTimeout(ctx, 5*time.Second),漏掉 cancel 变量,后续根本无处调用。
- 高频服务跑几天后,
runtime.NumGoroutine()会明显上涨 - pprof 查 heap 可能看到大量
timerCtx实例堆积 - Go 1.21+ 虽优化了部分场景,但 timer 泄漏仍存在
cancel() 应该在哪儿调?
不是“调一次就行”,而是“每个退出路径都要覆盖”。defer cancel() 是基础兜底,但不够。
立即学习“go语言免费学习笔记(深入)”;
- 函数有多个
return:每个分支前都应加cancel(),或统一收口 - panic 后 recover 的场景:
defer cancel()不会执行,需在 recover 块里补调 - HTTP handler 中:即使
ctx.Err() == context.DeadlineExceeded已发生,仍要调cancel() - 嵌套调用时:外层
cancel()不会自动清理内层创建的子 context,各自负责各自
WithTimeout 和 WithDeadline 怎么选?
绝大多数情况用 context.WithTimeout,别碰 context.WithDeadline,除非你真需要绝对时间点。
-
WithTimeout接收time.Duration,起点是调用它的那一刻,自动处理系统休眠、NTP 调整 -
WithDeadline接收time.Time,机器时间被回拨会导致提前取消;硬算time.Now().Add(...)再塞进去,等于白绕一圈 - HTTP 客户端、数据库驱动等标准库组件,只认
WithTimeout衍生的 ctx,不关心 deadline
超时起点不是你想象的那一刻
超时计时从 context.WithTimeout 执行开始,不是从 http.Do 或 db.QueryContext 开始。
- 如果中间有锁竞争、GC STW、DNS 解析阻塞,这些时间全算进超时窗口
- 想让“网络请求发出”才开始计时?那就别在 handler 入口建超时 ctx,而是在真正发起请求前一刻调
WithTimeout - 链路中嵌套多个
WithTimeout:外层超时包含内层构造开销,总耗时不等于各层之和
最容易被忽略的是:cancel() 的调用时机和频次,比怎么创建 ctx 更关键。它不是“可有可无的收尾”,而是资源生命周期的终结指令——漏一次,就多一个不会自动消失的 timer。


















