context.WithTimeout创建的定时器必须显式调用cancel释放,否则导致timer驻留、调度延迟和内存泄漏;cancel不仅关闭Done通道,还停止timer、清理子节点引用及弱引用;defer cancel必须无条件置于函数开头。

context.WithTimeout 创建的定时器资源不会自动释放,必须显式调用返回的 cancel 函数,否则底层 timer 会持续驻留 runtime,累积导致调度延迟和内存泄漏。
cancel() 不只是关闭 Done() 通道
很多人误以为 cancel() 的作用仅是让 ctx.Done() 关闭,从而让 select 能退出。实际上它还负责:
• 立即停止并回收内部 time.Timer(不是 time.After 那种一次性匿名 timer)
• 从父 Context 的子节点列表中移除该子 Context 引用
• 清理 goroutine 中可能持有的弱引用(如 context.cancelCtx 的 children map)
不调用 cancel(),哪怕超时已触发,timer 仍处于“已触发但未 Stop”状态,runtime 不会自动 GC 它。
defer cancel() 必须在创建后立即写,不能 conditional
常见错误是把 defer cancel() 放在条件分支里,或只在某些 error path 下调用。正确做法是:只要 context.WithTimeout 执行成功,cancel 就必须被 defer —— 即使后续逻辑 panic、return 或提前完成。
• 错误示例:if err != nil { cancel(); return } —— 忘了正常路径
• 正确姿势:ctx, cancel := context.WithTimeout(...); defer cancel(),放在函数开头附近
• 特别注意:HTTP handler 中若用 r.Context() 做 parent,再套 WithTimeout,cancel 也必须 defer,否则每个请求都漏一个 timer
与 time.Timer.Stop() 的行为差异
context.WithTimeout 内部用的是 time.NewTimer,其 Stop() 行为和手动管理 timer 不同:
• cancel() 会安全 Stop 已启动但未触发的 timer;如果 timer 已触发,cancel() 仍会尝试 Stop(返回 false,但无害)
• 手动调用 timer.Stop() 后还需 timer.Reset() 或 timer = nil,否则可能重复触发或泄漏
• context.WithDeadline 和 WithTimeout 共享同一套清理逻辑,但 WithCancel 不涉及 timer,所以没有这个问题
• 用 time.AfterFunc 完全无法 Stop,这是它不能替代 WithTimeout 的根本原因
如何验证 timer 是否泄漏
生产环境难直接观测 timer 对象,但可通过间接指标判断:
• pprof heap profile 中 time.timer 实例数随请求数线性增长
• runtime.NumGoroutine() 持续缓慢上升(因 timer 触发后需唤醒 goroutine 执行 callback)
• 使用 go tool trace 查看 timer goroutine 的生命周期是否异常延长
• 最简复现:起一个循环调用 WithTimeout 但不 cancel 的 goroutine,几秒后执行 debug.ReadGCStats 观察 NumGC 是否突增(timer 回收会触发额外 GC)











