time.ticker 是 go 中开销最低的内存级周期调度方案,适用于健康检查等场景;需用 select 监听 ticker.c 并配合 done 或 ctx.done() 通道优雅退出,避免并发堆积和 panic。

time.Ticker 是 Go 实现基于内存的周期性任务调度最直接、开销最低的选择,无需引入第三方库,也无需持久化或分布式能力。它适合服务内部健康检查、指标采集、缓存刷新等场景。
用 time.NewTicker 启动周期信号,但别直接 for range ticker.C
直接 for range ticker.C 看似简洁,但一旦任务执行时间超过 tick 间隔,下个信号会立刻触发,导致并发堆积甚至 panic(比如两个 goroutine 同时操作同一资源)。更糟的是,它无法响应退出信号。
- 必须用
select监听ticker.C,并配一个done或ctx.Done()通道用于优雅停止 - 任务逻辑应放在
case 分支内,避免阻塞整个 <code>select - 若任务耗时不可控(如 HTTP 调用),应在该分支内启动新 goroutine 异步执行,否则会跳过后续 tick
每次任务执行前检查是否已退出,别依赖 defer ticker.Stop()
defer ticker.Stop() 只在函数返回时调用,而 goroutine 可能长期运行 —— 如果你把 ticker 创建和 for select 写在一个 goroutine 里,defer 根本不会生效。真正有效的停止方式是:在 select 中监听退出信号,并在收到后显式调用 ticker.Stop(),再 return。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 错误写法:
go func() { defer ticker.Stop(); for range ticker.C { ... } }()→defer永远不执行 - 正确写法:在
case 分支中写 <code>ticker.Stop(); return - 额外建议:调用
ticker.Stop()后,可非阻塞读一次ticker.C(用select { case ),防止残留 tick 触发后续逻辑
避免用 time.Tick() 替代 time.NewTicker()
time.Tick(d) 返回一个只读 channel,底层仍创建了 *time.Ticker,但它没有暴露对象引用,无法调用 Stop()。这意味着只要 channel 还被引用,底层 ticker 就不会被 GC 回收 —— 在长生命周期服务中,这等于稳定泄漏 goroutine 和 timer 资源。
- 仅允许在极简 demo 或整个进程生命周期内都存在的场景使用(如 CLI 工具主循环)
- 所有服务端代码、goroutine 内部、需支持 reload/stop 的逻辑,一律用
time.NewTicker()+ 显式Stop() - 注意:
time.Tick()返回的 channel 是 unbuffered 的,多 goroutine 并发读会引发竞态,Go 不保证行为
单任务串行执行?用 time.AfterFunc 递归调度更安全
如果业务要求「严格串行」「不能并发」「不怕略微漂移」(比如每 10 秒清理一次本地缓存),time.AfterFunc 比 Ticker 更稳妥。它天然规避了 tick 积压问题:只有上一次任务完成,才安排下一次。
- 示例:
func run() { doWork(); time.AfterFunc(10*time.Second, run) } - 优点:无并发风险、无资源泄漏隐患(只要不逃逸出 goroutine)、任务失败不影响下次调度逻辑
- 缺点:无法对齐整点(如每分钟第 0 秒)、无法动态调整间隔、不适用于需要强周期语义的场景(如心跳上报)
- 关键点:若需支持取消,应传入
context.Context,并在doWork()开头检查ctx.Err()
真正容易被忽略的是:ticker.Stop() 必须和 goroutine 退出同步发生,而不是“记得要停”——它必须出现在你能 100% 确保执行到的路径上,比如 select 的某个 case 分支里。 否则,哪怕只漏掉一次,就可能让一个 ticker 在后台持续发送信号,直到进程结束。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










