time.After 在循环中反复调用会持续创建新 Timer,导致 goroutine 和 channel 积压,无法及时 GC;必须显式 Stop 或等待超时才能释放资源。

time.After 在循环里反复调用会堆积 Timer
time.After(d) 每次都新建一个 *time.Timer,背后绑着 goroutine 和 unbuffered channel。它不会因为变量离开作用域就释放——必须等到超时触发或被显式 Stop 才能被 GC 回收。
- 高频轮询场景(比如 HTTP handler 内部重试、连接心跳检测)中,
for { select { case 会导致每轮新增一个 Timer,未触发的 Timer 堆积在内存里 - pprof 查
runtime.timerprocgoroutine 数量持续上涨,heap profile 里time.Timer对象不断增长,就是典型泄漏信号 - 哪怕加了
default分支做非阻塞尝试,只要没读走time.After(...).C,Timer 就照常活着
time.NewTimer 必须配对调用 Stop()
手动创建的 timer := time.NewTimer(d) 看似可控,但漏掉 timer.Stop() 和乱用 timer.Reset() 同样泄漏。
-
timer.Stop()返回bool:true 表示还没触发,可安全丢弃;false 表示已触发或正在触发中,此时必须从timer.C读一次,否则 goroutine 卡在发送端 - 别在 defer 里只写
timer.Stop()就完事——如果函数 panic,defer 不执行;更稳妥的是if !timer.Stop() { 清理残留 - Reset 前不检查状态直接调用,可能让
timer.C积压多个未读时间戳,后续select阻塞在 channel 上
time.Ticker 必须显式 Stop(),且不能靠 defer 兜底
time.NewTicker(d) 启动后,背后的 goroutine 和 channel 是长驻的,GC 完全不回收——它和 time.Timer 的资源模型根本不同。
- 常见泄漏点:HTTP handler 启 goroutine 跑 ticker 做周期检查,handler 返回后 ticker 还在发信号;worker 启动时创建 ticker,但退出路径没覆盖
Stop() -
defer ticker.Stop()只对正常返回有效,panic 时失效;更可靠的是在select中监听ctx.Done(),收到信号后先ticker.Stop()再 return - 绝对不要用
time.Tick(d):文档明确标注 “leaks”,无法关闭,仅限一次性、无需回收的极简场景(如测试)
优先用 context.WithTimeout 替代裸 Timer
绝大多数“等待 + 超时”场景,context.WithTimeout 比手管 Timer 更安全、更少出错。
立即学习“go语言免费学习笔记(深入)”;
- 它自动管理底层 Timer 生命周期:context cancel 或超时触发时,Timer 会被 Stop,channel 关闭,无需你操心读或不读
- 与生态无缝集成:
http.Client、database/sql、grpc.Dial等都接受 context,语义统一 - 注意:不要把
ctx, cancel := context.WithTimeout(parent, d)的cancel函数漏掉——尤其在错误分支里,否则 context 泄漏同样拖垮 goroutine 数量
复用 Timer 或 Ticker 时,最容易被忽略的是 channel 积压和 panic 场景下的清理失效。不是写了 Stop() 就万事大吉,得看它是否真被执行、是否清掉了未读信号。


















