time.Timer 不适合周期性任务,因只触发一次;手动 Reset() 易丢事件、难对齐、易泄漏,应改用 time.Ticker。

time.Timer 不适合做周期性任务,它只触发一次;想“定时执行多次”,必须手动 Reset(),但容易丢事件、难对齐、易泄漏——别硬刚,该用 time.Ticker 就用。
为什么不能用 for { timer.Reset(); 模拟周期任务
看起来能跑,但实际埋了三个坑:
-
Reset()在 timer 已触发或已Stop()后返回false,不检查返回值就直接读timer.C,可能永久阻塞 - 每次
Reset()都是新起点,间隔误差会累积(比如本该每 5s 一次,第 3 次可能变成 5.012s,第 10 次偏差更大) - 没调
timer.Stop()就退出循环,底层 goroutine 和 timer 结构体不会被回收,内存和 goroutine 持续增长
time.Timer 的正确使用场景
它专为「单次延迟」而生,典型用法有三类:
- 超时控制:HTTP 客户端设置请求超时、数据库查询加
context.WithTimeout底层就是靠Timer - 延后执行:比如用户操作后 3 秒弹提示,用
time.AfterFunc(3*time.Second, showTip)更简洁(AfterFunc是Timer的封装) - 重试退避:配合指数退避逻辑,每次失败后
time.NewTimer(backoff),而不是复用一个 timer
注意:time.After(d) 返回的是 ,本质是 <code>NewTimer(d).C,但无法 Stop() —— 所以高频率、可取消的场景,必须用 NewTimer + 显式 Stop()。
立即学习“go语言免费学习笔记(深入)”;
timer.Reset() 怎么用才安全
只有两种情况值得复用 timer:
- 连接保活心跳:连接活跃时不断
Reset()延长超时,断开时Stop() - 状态机驱动的延迟切换:比如从 “空闲” 状态进入 “待确认”,启动 30s timer;若期间收到确认,就
Stop();若超时未确认,再发提醒并Reset()继续等
安全写法必须包含三步:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(5 * time.Second)
先 Stop(),再清空可能已就绪的 channel 值(防止后续读到过期时间),最后 Reset()。漏掉任何一步,都可能 panic 或逻辑错乱。
最常被忽略的细节:Stop 后的 channel 残留
timer.Stop() 只阻止未来发送,不消费已写入 timer.C 的值。如果 timer 刚触发、你立刻 Stop(),然后又去读 timer.C,就会拿到那个“过期时间”——这不是 bug,是设计如此。
所以所有带 Stop() 的代码,都要配套清理逻辑:
- 用
select { case 非阻塞尝试接收 - 或在
Stop()后加if len(timer.C) > 0 { (仅限无其他 goroutine 往里写时)
尤其在 HTTP handler 或短生命周期 goroutine 里启 timer,不清理残留,轻则逻辑误判,重则 panic: send on closed channel。


















