必须显式调用 cron.withseconds() 才能支持 6 字段秒级表达式,否则解析失败;推荐链式配置 cron.new(cron.withseconds(), cron.withchain(cron.recover(), cron.delayifstillrunning())) 并妥善管理任务 id 和调度器生命周期。

用 cron/v3 启动秒级定时任务,别漏掉 cron.WithSeconds()
默认的 cron.New() 只支持 5 字段 cron 表达式(分、时、日、月、周),写 "0 0 9 * * *" 会 panic:字段数不匹配。必须显式启用秒级解析。
- 正确写法:
c := cron.New(cron.WithSeconds()),之后才能用 6 字段表达式,比如"*/10 * * * * *"(每 10 秒) - 若坚持用传统 5 字段,得换 parser:
cron.New(cron.WithParser(cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow))) - 不加
WithSeconds()却传 6 字段表达式,错误信息是:parse error: field 6: expected 0-59 - 启动后记得
defer c.Stop(),否则进程退出时 goroutine 泄漏,下次重启可能堆积旧调度器
cron/v3 里任务 panic 不会停整个调度器,但得自己加 cron.Recover()
默认行为是 log panic 后继续跑下一个任务,看起来“稳”,但实际容易掩盖问题——比如数据库连接失效后反复重试失败,日志刷屏却没人发现。
- 推荐链式配置:
cron.New(cron.WithSeconds(), cron.WithChain(cron.Recover(), cron.DelayIfStillRunning())) -
cron.DelayIfStillRunning()防止上一轮没结束就触发下一轮,特别适合耗时不稳定的任务(如 HTTP 请求 + DB 写入) - 不加
Recover()时,panic 会打印到标准错误,但不会中断调度;加了之后,只吞当前任务 panic,不影响其他任务 - 注意:
cron.Recover()不捕获 panic 的堆栈,默认只打一行日志;如需完整堆栈,得自己 wrap 一个带debug.Stack()的 recoverer
Gin 路由里动态增删任务?别直接暴露 c.AddFunc(),先做 ID 管理
把 cron.Cron 实例挂到 Gin 的 context 或全局变量里,然后在 POST /task 里调 c.AddFunc(),看似可行,实则危险:任务没 ID 就无法后续控制,也无法查重、暂停或清理。
- 每次
AddFunc()返回一个cron.EntryID,必须存下来(比如 map[string]cron.EntryID 或数据库) - 增任务接口应返回该 ID,删/暂停接口用它做 key:
c.Remove(entryID)或自行标记状态 - 别在 handler 里直接传匿名函数,函数体无法序列化,重启后丢失;应封装成命名函数或结构体方法,再通过 ID 映射调用
- 如果任务需参数(如用户 ID、配置项),不要闭包捕获,改用结构体字段 +
Run()方法,方便持久化和重建
万级任务别硬扛,cron/v3 的排序瓶颈在 500+ 任务后就明显了
pprof 一跑就能看到 sort.Sort() 占 CPU 大头——cron/v3 每次 tick 都全量排序所有待执行任务,O(n log n) 成本随任务数线性恶化。
- 500 以内任务:放心用
cron/v3,简单可靠 - 500–5000:考虑降频,比如把“每秒检查”改成“每 5 秒检查”,用
cron.WithLocation(time.UTC)减少时区计算开销 - 5000+ 或要求毫秒级精度:换时间轮(TimeWheel)实现,比如
github.com/RichardKnop/machinery或自研轻量版;cron/v3此时已不是“不够好”,而是“架构错配” - 分布式多实例场景下,
cron/v3本身无协调能力,重复执行不可避免;真要跨节点唯一调度,得上go-quartz或带锁的 DB 调度层
真正难的不是写个 c.AddFunc(),而是让任务在重启、panic、扩容、故障时行为可预期。ID 管理、panic 捕获粒度、排序成本、分布式语义——这些点不提前想清楚,上线后 debug 的时间远超写代码的时间。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











