这里给你一个典型的循环内复用 timer 的例子,这是 NewTimer+Reset 最常见的使用场景:
1 | func worker(ctx context.Context, workCh <-chan Task) { |
关键点说明:
timer.Stop()的返回值:如果计时器已经到期或已被停止,返回false,这时候 channel 里可能还留着一个未被读取的值(旧版本行为),需要手动 drain 掉再 Reset,否则下一次 select 可能立刻读到这个旧值。Go 1.23+ 的简化:由于 timer channel 现在是同步的,
Stop/Reset返回后能保证不会再收到过期值,所以在新版本里,上面这段 drain 逻辑理论上可以省略,直接:
1 | timer.Reset(5 * time.Second) |
但如果你的代码库要兼容旧版本 Go,保留 drain 逻辑是更安全的写法。
defer timer.Stop():即使 Go 1.23 之后未 Stop 的 timer 能被 GC,显式 Stop 依然是好习惯——能让底层的 timer 立刻从运行时的堆里摘除,而不是等 GC 扫描到。
如果只是简单的"一次性超时",不涉及循环复用,直接用 time.After 或者一次性的 NewTimer 就足够了,不需要这套 Reset 逻辑。
Go 1.27 已经在 2026 年 8 月 19 日正式发布,而且这个版本里有个关键变化:asynctimerchan 这个 GODEBUG 开关被彻底移除了——也就是说 Go 1.23 引入的"新计时器实现"(可被 GC 回收 + 同步 channel)在 1.27 里变成了唯一行为,不再有任何回退旧实现的选项。
回到最初的问题:Stop() 还需要调用吗?——需要,而且仍然是最佳实践。 原因和"会不会内存泄漏"是两件事:
1. GC 回收 ≠ 立即释放
即使不 Stop,不再被引用的 timer 最终会被 GC 清理掉,但这依赖于 GC 扫描周期。如果你的程序创建 timer 很频繁,不 Stop 意味着这些对象要一直等到下一轮 GC 才能真正释放,期间仍然占用内存和运行时的计时器堆(timer heap)空间,增加不必要的 GC 压力。显式 Stop 能让资源立刻释放。
2. Ticker 是完全不同的情况,必须 Stop
这是最容易踩坑的地方。Ticker 会持续触发,只要它还被某个存活的 goroutine 引用(比如一个 for { select { case <-ticker.C: ... } } 循环),它就是可达的,GC 根本不会回收它,会一直触发下去,浪费 CPU 唤醒和调度开销。Go 1.23+ 的自动回收解决的是"对象不可达时被 GC 捡走",但只要 ticker 还在被使用中的 goroutine 持有,它就不会被自动回收——这种情况下不 Stop 就是真正的资源泄漏,和 Go 版本无关。
1 | func poll() { |
3. AfterFunc 场景下 Stop 直接影响程序逻辑time.AfterFunc 到期后会在新 goroutine 里执行回调,如果你想在某个条件下取消这个回调执行,必须调用 Stop(),这和内存回收完全无关,是功能正确性问题。
总结一句话:Go 1.27 把"未 Stop 的 timer/ticker 不会造成内存泄漏"这件事变成了语言运行时的永久保证,你不再需要担心 time.After 在 select 里被跳过导致的经典泄漏。但 Stop() 依然是推荐的显式资源管理习惯——尤其是 Ticker,不 Stop 会导致它无限期继续触发,这跟 GC 能不能回收内存没有关系。简单说:一次性的 Timer/time.After 现在"忘记 Stop 问题不大",但 Ticker 永远要 Stop。
