必须调用 timer.Stop():即使 timer 已触发或 timer.C 已读取,不显式 Stop 会导致 goroutine 和 channel 泄漏,GC 无法回收,压测时 goroutine 数持续增长。

timer.Stop() 必须在 timer 触发后也调用
很多人误以为 timer 已触发(timer.C 已被接收)就不用 timer.Stop() 了,其实不然。Go 的 time.Timer 底层持有 goroutine 和 channel,即使已触发,若不显式 Stop(),仍可能保留对 channel 的弱引用,导致 GC 无法回收相关资源;压测时 goroutine 数缓慢上涨,pprof 中可见残留的 runtime.timerproc。
正确做法是:无论是否已触发、是否已读 timer.C,只要不再需要该 timer,就立即调用 timer.Stop()。
- 错误写法:
select { case —— 缺少 <code>timer.Stop() - 正确写法:
select { case - 更稳妥写法(尤其配合 defer):
defer func() { if !timer.Stop() { ,用于处理已触发但 channel 尚未读完的情况
Reset 前必须先 Stop 并检查返回值
timer.Reset() 不是原子安全操作。如果 timer 已触发(timer.C 已关闭),直接 Reset() 会成功返回 true,但底层旧 goroutine 仍可能往已关闭的 channel 发送时间值 → panic: send on closed channel。
唯一安全的重用模式是:
- 始终先调
timer.Stop(),并检查其返回值 - 仅当
timer.Stop()返回false(说明已触发或已 stop),才需额外消费一次timer.C(避免阻塞) - 再调
timer.Reset(newDur)
示例:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(3 * time.Second)
在 select 中监听 timer.C 时 Stop 的时机很关键
常见陷阱:把 timer.Stop() 写在 select 的某个 case 分支里,而其他分支(比如 ctx.Done())退出时漏掉 Stop。
正确姿势是让 Stop() 脱离分支逻辑,确保无论从哪个出口离开,timer 都被清理:
- 定义 timer 在函数作用域顶部,用
defer timer.Stop()(注意:panic 时 defer 不执行,慎用于关键路径) - 更健壮的做法:在所有可能的退出路径前显式调用
timer.Stop(),包括return、break、goto或 context 取消分支 - 若 timer 生命周期与 context 绑定,务必在
case 分支末尾加 <code>timer.Stop()
time.After 和 time.AfterFunc 不需要手动 Stop
time.After(d) 返回的是一个 <chan time.time></chan>,它内部封装了 time.Timer,且在触发后自动清理;time.AfterFunc(d, f) 同理,函数执行完即释放资源。这两者都不暴露 timer 实例,因此不存在忘记 Stop() 的问题。
但它们也不支持取消重置、无法参与多路复用 select(除非你只关心超时)、也不能获取触发时刻。需要这些能力时,必须退回到 time.NewTimer + 手动 Stop() 的组合。
一句话判断:只做“等 X 秒后干件事”,优先用 time.AfterFunc;要控制生命周期、参与 select、动态调整时长,就必须亲手管好 timer.Stop()。
真正容易出事的,从来不是第一次创建 timer,而是 long-running 服务中反复 Reset() 却没人检查 Stop() 返回值、也没清空已触发的 timer.C 缓冲 —— 这类 bug 往往上线后数周才在低概率并发场景下暴露。


















