time.Timer/Ticker 与定时任务有哪些常见坑?

一句话结论:Timer 一次性、Ticker 周期触发,两者都挂在 runtime 的时间堆上:忘记 Stop 会白白占着定时器;Ticker 必须 defer Stop;循环里别用 time.After(每次分配新 Timer);复用的 Timer 要正确 Reset;回调里做重活会丢 tick。Go 1.23 调整了 Timer/Ticker 的通道语义(改为同步无缓冲),新版代码可少走老版本的“drain channel”模板。

常见坑与对策

现象对策
Ticker 不 Stopruntime 定时器持续存在defer ticker.Stop() 是铁律
Timer 用完不 Stop长定时器迟迟不释放提前结束时 defer t.Stop()
循环里 time.After每次迭代分配一个 Timer改用 Ticker 或复用一个 Timer
复用 Timer 的 Reset老版本残留值可能“提前触发一次”旧版本配合 Stop+drain;1.23 后语义已简化
回调执行过久触发被合并/跳过回调只投递信号,重活另起 goroutine
依赖毫秒级准点Ticker 只是尽力而为高精度场景评估漂移,必要时自实现

另外:time.Sleep 不可被取消,想“随时可中断的等待”要用 select 加 timer 或 ctx.Done。

安全的复用写法

func waitWithTimer(ctx context.Context, d time.Duration) bool {
	t := time.NewTimer(d)
	defer t.Stop() // 提前返回也要释放
	select {
	case <-t.C:
		return true // 等到了
	case <-ctx.Done():
		return false // 被取消
	}
}

func main() {
	tk := time.NewTicker(time.Second)
	defer tk.Stop() // 别忘了
	for range tk.C {
		// 如果这里很耗时,期间的 tick 会被合并/跳过
	}
}

常见追问 / 记忆点

  • 追问:定时任务里要做耗时操作怎么办?——回调内只投递信号,真正工作丢给 worker,避免阻塞下一次触发。
  • 追问:time.After 与 time.NewTimer 什么区别?——After 是一次性简写且每次分配,适合低频;高频循环自建 Timer 复用。
  • 记忆点:Ticker 必 Stop、循环别 After、复用要 Reset、回调别阻塞。
笔记加载中…