time.ticker 不适合做定时任务,因其仅发信号、不防重、不处理失败、不支持表达式;应使用 cron/v3 或 gocron,需正确初始化、启动、防并发、热更新及上下文超时控制。

别直接用 time.Ticker 或 time.Timer 写“定时任务”——它们只是计时原语,不是调度器。真要跑“每天 9 点发邮件”“每 5 分钟拉一次数据”,得用 cron/v3 或 gocron,否则大概率漏执行、并发冲突、goroutine 泄漏。
为什么 time.Ticker 不适合当定时任务用
time.Ticker 只保证“每 N 秒发一个信号”,不判断时间点、不防重、不处理失败、不支持表达式。常见翻车点:
- 任务执行耗时 > 间隔(比如接口超时 8 秒,但 ticker 是 5 秒一 tick),后续 tick 会被丢弃,channel 缓冲区满后 ticker goroutine 持续忙等,CPU 悄悄升高
- 没调
ticker.Stop()—— 进程退出后 goroutine 还在后台跑,pprof 里能看到一堆runtime.timerproc - 想实现“每月第一个周一 10 点执行”,靠每分钟 tick + 手动算日期,逻辑臃肿还容易错
-
for range ticker.C里直接调doSomething(),一旦 panic,整个循环卡死,后续 tick 全停
robfig/cron/v3 怎么初始化才不踩坑
这是目前最稳的 Go cron 库,但初始化不对,任务压根不跑。关键三步必须全做:
- 用
cron.New(cron.WithSeconds())启用秒级支持(默认只认 5 字段,写"*/5 * * * *"会静默失败) - 必须显式调
c.Start()——cron.New()创建的是暂停态调度器,不Start()就等于没开 - 主 goroutine 不能立刻退出,得
select {}或signal.Notify阻塞住,否则进程闪退,任务一次都不触发 - 容器环境务必装
tzdata并设TZ=Asia/Shanghai,否则time.Local退化成 UTC,本地测试正常、上线全乱
任务函数里 panic 了怎么办
cron/v3 默认捕获 panic 并 log,不会让整个调度器崩掉,但前提是别用 AddFunc 写复杂逻辑。更稳妥的做法:
- 用
cron.WithChain(cron.Recover(), cron.SkipIfStillRunning())包一层,自动 recover + 防并发重复执行 - 需要传参或状态管理,别闭包捕获循环变量(
for _, job := range jobs { c.AddFunc("0 0 * * *", func() { fmt.Println(job.Name) }) }会全打最后一个值),改用AddJob实现cron.Job接口 - HTTP/DB 调用必须复用 client 和连接池,别在任务函数里新建
http.Client或sql.Open,否则 fd 耗尽、超时堆积
怎么让定时任务支持热更新配置
硬编码 "0 0 9 * * *" 在代码里,等于放弃运维能力。真要热重载,得靠外部驱动:
- 把 cron 表达式存在 Redis 或 etcd,用
fsnotify监听配置文件变化,或定期轮询读取 - 每次变更时,先
c.RemoveJob()停旧任务,再c.AddFunc()加新任务(注意:别直接反射改内部字段,v3 不保证兼容) - 若用
gocron,它原生支持.StopJobs()和.Start()重载,比手动管理更干净 - 所有任务函数必须带 context,超时控制写死在代码里不如从配置中心动态拉取
真正难的不是“怎么启动一个定时任务”,而是“怎么让它在服务重启、网络抖动、时区切换、配置变更时依然可靠运行”。这些细节藏在 Stop() 调不调、WithSeconds() 加不加、context.WithTimeout() 包不包里面——漏一个,线上就可能半夜告警。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











