time.ticker 不会自动停止,必须手动调用 stop() 否则导致 goroutine 泄漏;timer 可复用可取消,after 是一次性不可取消的封装。

time.Ticker 不会自动停止,不手动 Stop() 就泄漏 goroutine
很多人以为 Ticker 在作用域结束或变量被回收时会自动停掉,其实完全不会。Ticker.C 是一个持续发送时间戳的无缓冲 channel,只要没调用 Stop(),底层 goroutine 就一直跑着,往 channel 里塞值——而如果没人收,它就永远卡在发送上,goroutine 永不退出。
常见错误现象:
– 程序内存缓慢上涨,pprof 查到一堆阻塞在 runtime.chansend 的 goroutine
– 单元测试跑完后进程不退出(因为 ticker goroutine 还活着)
- 所有
time.NewTicker后,必须配对调用ticker.Stop(),哪怕只用一次 - 在 defer 中调用最安全:
defer ticker.Stop() - 不要在循环里反复
NewTicker却不Stop前一个(比如配置热更新场景) - 注意:
Stop()可重复调用,多次调用无副作用
time.Timer 和 time.After 的语义差异容易混淆
Timer 是可复用、可重置的对象;time.After 是一次性、不可取消的封装。很多人拿 After 当“轻量 Timer”用,结果发现没法取消、也没法重用。
使用场景对比:
– 需要取消超时(比如 HTTP 请求中途取消)→ 必须用 Timer + Stop() 或 Reset()
– 只是简单延时执行一次,且确定不需要干预 → time.After 更简洁
立即学习“go语言免费学习笔记(深入)”;
-
time.After(d)底层也创建了Timer,但它没暴露对象,你拿不到引用,也就无法Stop() -
Timer.Reset()在 Go 1.22+ 支持并发安全,但旧版本中若在C已触发后立刻Reset(),可能 panic;建议先Stop()再Reset() - 别写
timer := time.NewTimer(d); —— 这样漏掉了第一次触发后的清理,可能双触发
time.Ticker 的精度受系统调度和 GC 影响,别当实时钟用
Ticker 不是硬件定时器,它依赖 Go runtime 的调度器唤醒,实际间隔可能比设定值略长,尤其在高负载、频繁 GC 或大量 goroutine 抢占时。你看到日志里 “每 100ms 打点”,结果采样发现间隔在 105~130ms 波动,这正常。
性能影响:
– Ticker 自身开销极小,但每次触发都涉及 channel 发送 + 调度唤醒
– 如果 handler 函数执行时间 > tick 间隔(比如每 100ms 触发,但 handler 耗时 150ms),后续 tick 会排队,C channel 缓冲为 1,第 2 个 tick 会被丢弃(Ticker 不做队列累积)
- 高频 ticker(如
- 需要严格周期行为(如限流、心跳),应在 handler 开头记录
time.Now(),而不是假设“此刻就是 tick 时刻” - 避免在 ticker handler 中做同步 I/O、锁竞争或大内存分配,否则放大抖动
time.AfterFunc 和 timer.Reset 的竞态隐患
time.AfterFunc(d, f) 是语法糖,等价于 time.NewTimer(d).Stop() 后立即触发函数,但它返回的是 void,你拿不到 Timer 引用。这意味着:你想中途取消它?做不到。
更隐蔽的问题是 Reset() 使用不当:
– 如果 timer 已触发(C 已关闭),再调用 Reset() 在老版本 Go 会 panic
– 如果 timer 未触发,但你同时在另一个 goroutine 调用 Stop() 和 Reset(),可能因非原子性导致漏触发或 double trigger
- 需要可取消的延时任务,直接用
time.NewTimer,自己管理生命周期 - 检查是否已触发,用
select { case 非阻塞探测,别依赖 <code>timer.Stop()返回值(它只返回是否成功停止) - Go 1.22+ 起
Reset()允许在已触发 timer 上安全调用,但仍建议统一模式:先Stop(),再Reset()
真正麻烦的从来不是怎么启动一个定时器,而是谁负责关、什么时候关、关了之后 channel 还有没有人读——这些边界稍不留意,就变成深夜看 pprof 的理由。










