必须用cron.new(cron.withseconds()),因默认仅支持5字段(分时日月周),不加该选项时6字段表达式(如"/10 ")会报错“expected exactly 5 fields, found 6”;国内业务普遍需秒级触发,且统一用6字段可避免环境间格式不一致导致的静默失败。

为什么 cron.New() 必须加 cron.WithSeconds()
默认 cron.New() 只支持 5 段表达式(分、时、日、月、周),写成 "*/10 * * * * *" 会直接报错:expected exactly 5 fields, found 6。国内业务几乎都要求秒级触发(比如每 30 秒拉一次支付状态、每 5 秒检查队列积压),不加 cron.WithSeconds() 就跑不起来。
即使你只需要分钟级任务,也建议统一用 6 段格式,例如 "0 */2 * * * *"(第 1 位是秒,填 0 即可),避免开发/测试/生产环境因表达式格式不一致导致静默失败。
- 别用
cron.New()空参初始化,必须显式传cron.WithSeconds() - 加了之后才支持
"*/5 * * * * *"这类带秒的写法 - 如果真要 UTC 时间,再额外加
cron.WithLocation(time.UTC);国内服务一般用time.Local或time.FixedZone("CST", 8*60*60)
c.Start() 放错位置会导致 panic 或重复启动
c.Start() 是异步启动 goroutine,本身不阻塞 Gin 启动,但**绝不能放在路由 handler 里**——每次 HTTP 请求都调一次,会触发 cron: duplicate entry panic,且 goroutine 泄漏无法回收。
正确时机只有一处:Gin 路由注册完、r.Run() 之前,且必须配对 defer c.Stop(),否则进程退出时调度器还在后台跑,日志可能卡住、数据库连接不释放。
- 在
main()函数末尾、r.Run()前调用c.Start() - 紧跟其后写
defer c.Stop(),哪怕程序 panic 也能确保清理 - 不要在中间件、handler、init 函数里调
c.Start()
AddFunc 和 AddJob 怎么选?看要不要传参和 context
AddFunc 只接受无参函数,适合 log.Println("ping") 这类轻量操作;一旦涉及数据库、HTTP 客户端、超时控制,就必须用 AddJob + 自定义结构体。
原因很实际:没 context.Context 的任务一旦卡住(比如 DB 查询 hang 死),整个 cron 调度器线程会被拖住,后续所有任务全延迟。而 AddJob 允许你在 Run() 方法里自己做 context.WithTimeout。
- 简单打印、发通知 → 用
c.AddFunc("0 2 * * *", func() { ... }) - 要访问
*sql.DB、http.Client、设超时 → 封装结构体实现cron.Job接口 - 结构体字段里存依赖,
Run()里用ctx, cancel := context.WithTimeout(...),记得defer cancel()
多实例部署时任务重复执行,光靠 cron 库解决不了
robfig/cron/v3 是纯单机调度器,没有分布式锁或协调机制。如果你的 Gin 服务起 3 个 Pod,每个都会独立执行 "0 2 * * *" 归档任务——结果就是数据被删三次、报表生成三份。
临时方案可用环境变量开关:if os.Getenv("ENABLE_CRON") == "true" { c.Start() },只让某台机器执行;但长期必须换方案,自己用 Redis SETNX 写锁极易出错(锁残留、误删、竞争条件)。
- 短期:用配置中心或环境变量控制单点执行
- 中期:换
go-quartz,它原生支持集群模式和选举 - 长期:对接 xxl-job-executor-go,由中心调度器统一分配执行节点
真正容易被忽略的是:cron 本身不提供任何可观测性。没加 cron.WithChain(cron.Recover(cron.DefaultLogger)),任务 panic 后就静默消失,连日志都没有。这个 recover 链必须加,不然线上排查等于盲人摸象。











