会堆积内存。time.after 每次调用都新建 timer,绑定 goroutine 和 channel;循环中频繁使用时,未触发的 timer 会滞留至超时才被 gc,导致内存泄漏。
time.after 在循环里反复调用会堆积 timer
time.after(d) 看似简洁,但每次调用都会新建一个 time.timer,它背后绑着一个 goroutine 和一个 channel。在 for 循环中无节制使用(比如轮询超时),未触发的 timer 会一直等到超时才被 gc —— 而高频下大量“永远等不到触发”的 timer 就卡在内存里。
- 典型错误:HTTP handler 内部循环中写
case - 后果:QPS 上千时,每秒新增上千个 Timer,内存持续上涨,GC 压力陡增
- 替代方案优先选
context.WithTimeout,它不暴露底层 Timer,生命周期由 context 控制 - 若必须用 Timer,应初始化一次,用
timer.Reset(d)复用,且 Reset 前需确保 Timer 已停止或已触发
time.NewTicker 必须显式调用 Stop()
time.Ticker 启动后会持续向 ticker.C 发送时间戳,背后始终运行一个 goroutine。它不会因为变量离开作用域、函数返回或 panic 自动停止 —— 只要没调 ticker.Stop(),它就一直活着。
- 常见泄漏点:goroutine 中启 Ticker 做周期检查,但主逻辑退出时忘了 Stop
-
defer ticker.Stop()是安全底线,但仅对正常返回有效;panic 时 defer 不执行 - 更稳妥做法是结合 context:
for range ticker.C改为select { case - 绝对不要用
time.Tick(d),文档明确写它“leaks”,无法关闭,仅限一次性、无需回收的场景
timer.Reset() 的返回值必须检查
timer.Reset(d) 返回 bool:false 表示 Timer 已触发或已停止。忽略返回值直接 Reset,可能导致 timer.C 积压未读消息,后续阻塞在 上。
- 正确模式:先
if !timer.Stop() { 清空可能残留的信号,再 <code>timer.Reset(d) - 注意:Reset 不能用于刚创建但尚未触发的 Timer(此时 Stop 返回 true),但也不能假设它一定成功 —— 必须按返回值分支处理
- 如果复用 Timer 的 goroutine 频繁 Reset,还要确保 channel 读取及时,否则积压会拖慢整个 select 轮询
Timer 和 Ticker 的底层资源差异
time.Timer 在无强引用时可被 GC 回收,而 time.Ticker 不行 —— 它的 goroutine 和 channel 是长驻的,必须靠 Stop() 主动切断。这是两者最根本的资源管理区别。
- Timer 泄漏多因“创建太多、不用不管”,Ticker 泄漏多因“启了不关”
- 调试时可查
runtime.NumGoroutine()是否随时间线性增长,配合 pprof 查 goroutine stack 找出未 Stop 的 Ticker - 单元测试里容易漏掉 Stop:哪怕只跑一次
range ticker.C,也得配defer ticker.Stop(),否则测试进程退出前 Ticker 仍在发信号
真正难发现的不是泄漏本身,而是泄漏发生的位置 —— 它往往藏在嵌套 goroutine、中间件封装、或 panic 恢复逻辑之后。Stop 调用必须出现在所有退出路径上,包括 recover 分支。











