time.Ticker适合固定间隔后台轮询,需用goroutine异步处理避免阻塞;time.AfterFunc仅适用于单次延迟,重复调度应优先选Ticker;复杂场景宜用robfig/cron/v3等第三方库;务必配对Stop防止goroutine泄漏。

time.Ticker 适合固定间隔的后台轮询任务
当需要每秒、每分钟执行一次逻辑(比如健康检查、指标采集),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 := doHealthCheck(); err != nil { log.Printf("health check failed: %v", err) } }() } }()
注意:ticker.Stop() 要在退出前调用,否则 goroutine 泄漏;doHealthCheck 这类函数内部也应设超时,防止 goroutine 积压。
time.AfterFunc 不适合重复调度
time.AfterFunc 只触发一次,常被误用作“定时器重启”方案。例如有人写:
立即学习“go语言免费学习笔记(深入)”;
func scheduleEvery5s() {
time.AfterFunc(5*time.Second, func() {
doWork()
scheduleEvery5s() // ❌ 递归调用无控制,易栈溢出或 goroutine 爆炸
})
}这不仅难以取消,还可能因 doWork 延迟导致调度漂移加剧。真正需要重复调度时,优先选 time.Ticker;若只需单次延迟执行(如 10 秒后发告警),才用 time.AfterFunc。
- 取消方式:保存返回的
*time.Timer,调用timer.Stop() - 不要依赖
AfterFunc的“精确性”——它只保证“至少等待这么久”,不承诺唤醒时间点
复杂调度建议用第三方库,别硬套 time 包
time.Ticker 和 time.Timer 仅支持简单周期或单次延迟。遇到以下场景,硬实现会迅速失控:
- 每天凌晨 2:15 执行备份(需 cron 表达式解析)
- 任务失败后按指数退避重试(需状态管理)
- 多个任务间有依赖或互斥要求
推荐直接使用 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() 开启秒级精度,而默认版本只到分钟级——这点容易忽略,导致你以为写了 "* * * * *" 就能每秒跑,实际是每分钟跑一次。
goroutine 泄漏是定时任务最隐蔽的坑
所有基于 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控制整个调度生命周期,而不是靠全局变量或标志位 - 避免在定时器回调中启动无缓冲 channel 的发送——如果接收方卡住,发送 goroutine 就永远挂起
真正难调试的不是功能写不对,而是几个月后服务内存缓慢上涨、goroutine 数突破万级却找不到源头。










