正确做法是用 select + case 配合 ticker.C,而非直接阻塞读取 ticker.C,以避免 goroutine 泄漏和调度问题。

Go 里用 time.Ticker 做周期性任务,核心就一条:别直接用 ticker.C 阻塞读,得配合 select + case ,否则容易卡死或漏触发。
为什么不能直接 for-range 或 for {
因为 ticker.C 是一个无缓冲 channel,每次发送都会阻塞,直到被接收。如果接收端逻辑耗时、panic 或提前退出,ticker 会持续堆积 goroutine(内部定时器仍运行),最终导致内存上涨甚至程序假死。
- 常见错误写法:
for range ticker.C { ... }—— 一旦循环体 panic,ticker就没人收了 - 更隐蔽的坑:
for { —— <code>doWork()耗时超过 tick 间隔时,下一次会立刻命中,看似“没丢”,实则已积压,且无法感知是否滞后 - 正确姿势必须是
select,才能安全响应退出信号或超时控制
怎么安全启动并停止 time.Ticker
启动和停止必须成对,且停止要确保在所有接收逻辑退出后调用 ticker.Stop(),否则资源泄漏。
- 启动:
ticker := time.NewTicker(5 * time.Second),间隔从创建时刻开始计 - 接收必须用
select,例如:for { select { case <-ticker.C: doSomething() case <-done: // 外部传入的 context.Done() 或关闭 channel return } } - 停止时机:在
select退出后立即调用ticker.Stop();如果用context.Context,建议用time.AfterFunc或封装为带 cancel 的 helper,避免忘记 stop
time.Ticker 和 time.Timer 到底该选谁
本质区别:前者是「重复滴答」,后者是「只响一次」。别为了省事用 Timer + 循环重置模拟 Ticker,它不保证严格周期,误差会累积。
立即学习“go语言免费学习笔记(深入)”;
- 用
Ticker:轮询健康检查、定期刷新缓存、心跳上报等要求稳定间隔的场景 - 用
Timer:延迟执行、超时控制、重试退避(如指数退避用time.AfterFunc更清晰) - 注意:
Ticker不支持动态改间隔,要改就得Stop()后重建;而Timer.Reset()可以复用
真正容易被忽略的是:哪怕你用了 select,如果 doSomething() 执行时间 > tick 间隔,ticker.C 里的值会一直堆积——这不是 bug,是设计使然。你需要自己判断是否跳过、合并或记录滞后量,Go 不替你做业务决策。


















