time.Ticker适合固定间隔后台轮询,需用goroutine异步处理避免阻塞;time.AfterFunc仅适用于单次延迟,重复调度应优先选Ticker;复杂场景宜用robfig/cron/v3等第三方库;务必配对Stop防止goroutine泄漏。
当需要每秒、每分钟执行一次逻辑(比如健康检查、指标采集),time.Ticker 是最轻量的选择。它不保证绝对准时,但能维持稳定的平均间隔,且资源开销极小。
常见错误是直接在 for range ticker.C 循环里做耗时操作,导致下一次触发被阻塞。必须用 goroutine 并发处理:
ticker := time.NewTicker(30 * time.Second) defer ticker.Stop()go func() { for range ticker.C { // 必须异步执行,避免阻塞 ticker go func() { if err := doHea
lthCheck(); err != nil { log.Printf("health check failed: %v", err) } }() } }()
注意:ticker.Stop() 要在退出前调用,否则 goroutine 泄漏;doHealthCheck 这类函数内部也应设超时,防止 goroutine 积压。
time.AfterFunc 只触发一次,常被误用作“定时器重启”方案。例如有人写:
func scheduleEvery5s() {
time.AfterFunc(5*time.Second, func() {
doWork()
scheduleEvery5s() // ❌ 递归调用无控制,易栈溢出或 goroutine 爆炸
})
}这不仅难以取消,还可能因 doWork 延迟导致调度漂移加剧。真正需要重复调度时,优先选 time.Ticker;若只需单次延迟执行(如 10 秒后发告警),才用 time.AfterFunc。
*time.Timer,调用 timer.Stop()
AfterFunc 的“精确性”——它只保证“至少等待这么久”,不承诺唤醒时间点time.Ticker 和 time.Timer 仅支持简单周期或单次延迟。遇到以下场景,硬实现会迅速失控:
推荐直接使用 robfig/cron/v3 或 github.com/hibiken/asynq(带持久化和重试)。例如用 cron 启动一个每日任务:
c := cron.New(cron.WithSeconds())
c.AddFunc("0 15 2 * * *", func() { // 秒 分 时 日 月 周
backupDB()
})
c.Start()
defer c.Stop()它的 WithSeconds() 开启秒级精度,而默认版本只到分钟级——这点容易忽略,导致你以为写了 "* * * * *" 就能每秒跑,实际是每分钟跑一次。
所有基于 time.Ticker 或 time.Timer 的长期运行任务,只要没正确关闭通道或停止定时器,就会持续生成 goroutine。用 go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=1 能快速确认是否泄漏。
关键防御点:
time.NewTicker / time.NewTimer 都配对 defer xxx.Stop()(注意:要放在 goroutine 外层,不能在循环内 defer)context.WithCancel 控制整个调度生命周期,而不是靠全局变量或标志位真正难调试的不是功能写不对,而是几个月后服务内存缓慢上涨、goroutine 数突破万级却找不到源头。