robfig/cron 已归档且存在调度漂移、panic崩溃、时区混乱三大问题,官方推荐改用gocron或原生time.ticker+time.afterfunc;gocron支持秒级、时区、单job控制,语法兼容;简单周期任务优先用原生方案。

robfig/cron 已归档,新项目别用它——Go 官方推荐用 gocron 或原生 time.Ticker + time.AfterFunc 组合,否则上线后会踩到调度漂移、panic 崩溃、时区混乱三连坑。
为什么 robfig/cron 不能继续用
这个库早在 2021 年就由作者标记为 archived,不再维护。最致命的是它内部用 time.Parse 解析 cron 表达式时没做严格校验,遇到 0 0 * * * 这类跨月边界场景,可能漏触发;更糟的是,它依赖已弃用的 github.com/robfig/cron/v3 分支,而 v3 实际上和 v2 接口不兼容,func New() 返回类型都变了。
- 常见错误现象:
panic: runtime error: invalid memory address or nil pointer dereference(尤其在 reload job 时) - 时区默认是本地时区,但 Docker 容器里常是 UTC,导致定时任务比预期晚 8 小时
- 不支持秒级精度(v2 默认最小粒度是分钟),想每 10 秒跑一次?做不到
替代方案:用 gocron 替代 robfig/cron
gocron 是目前最活跃的 Go 定时库,API 清晰、支持秒级、内置时区、可停用/重启单个 job,且和 robfig/cron 的表达式语法完全兼容。
- 安装:
go get github.com/go-co-op/gocron - 基础用法:
s := gocron.NewScheduler(time.UTC)—— 一定显式传时区,别依赖默认值 - 加任务:
s.Cron("0 */5 * * * ?").Do(func(){ ... })(注意:v2 表达式末尾多一个?支持秒位) - 启动前必须调
s.Start(),否则啥也不干;停止用s.Stop(),不是s.Close() - 性能影响:每个 job 单独 goroutine,100 个 job 就有 100 个常驻 goroutine,别无脑塞几百个
简单场景直接用原生 time.Ticker
如果只是“每 30 秒查一次健康状态”或“每分钟打个日志”,根本不需要引入第三方库。原生方案更轻、可控、无隐藏行为。
- 示例:
s := time.NewTicker(30 * time.Second) defer s.Stop() for range s.C { // 执行逻辑 if err := doHealthCheck(); err != nil { log.Printf("health check failed: %v", err) } } - 注意:
time.Ticker不保证绝对准时,只保证“间隔大致稳定”。如果某次执行耗时 > 间隔,下一次会立刻补上(不会跳过) - 无法按日历调度(比如“每月 1 号凌晨 2 点”),这种必须用
gocron或自己算time.Until(nextRunTime)
容易被忽略的时区和并发问题
几乎所有线上故障都出在这两点:一是没设时区,二是 job 函数里改了共享变量却没加锁。
- 时区必须统一初始化:
gocron.NewScheduler(time.FixedZone("CST", 8*60*60)),别信time.Local,容器里它大概率是 UTC - job 函数默认并发执行,如果多个 job 都写同一个 map,必 panic;要么加
sync.Mutex,要么用s.SingletonMode()让同一 job 不重入 - reload job 时别直接
s.RemoveByTag后立刻s.Cron(...).Tag(...),中间有空窗期;应先s.Stop(),改完再s.Start()
robfig/cron 的坑不在语法难,而在它把“调度正确性”藏得太深——你以为写对了 cron 表达式就万事大吉,其实底层时钟推进、goroutine 生命周期、panic 恢复全得自己兜底。换库不是折腾,是止损。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











