cron.AddFunc 中 panic 会导致调度器静默停摆,因 v3 默认串行执行且无自动 recover;需用 WithChain(cron.Recover(logger)) 统一捕获,而非手动 defer recover。

为什么 cron.AddFunc 里 panic 会导致整个调度器停摆
cron/v3 默认串行执行所有任务,且每个任务外层没有自动 recover。一旦某个 func() 内部发生 panic(比如空指针、除零、map 写并发),当前 goroutine 立即终止,c.Run() 流程退出,后续所有任务永久不再触发——没有日志、不报错、不重启,只是“静默死亡”。
常见错误写法:c.AddFunc("0 * * * *", func() { panic("oops") }),跑一两次就停了,你还以为是表达式写错了。
- 它只影响当前任务所在的 goroutine,但 cron 的主循环也卡在这儿,不会自动重试或跳过
- panic 不会传播到 main goroutine,所以程序还在运行,但定时器已失效
- 默认 logger 只记录启动/注册日志,不记录 panic,你得靠监控或手动验证触发时间才能发现
如何给 cron 任务加 recover:必须用 WithChain + Recover
别自己封装 defer recover() 到每个 AddFunc 里——太重复、易漏、难维护。v3 提供了链式中间件机制,正确做法是在初始化时统一注入:
c := cron.New(
cron.WithLocation(time.Local),
cron.WithChain(cron.Recover(cron.DefaultLogger)),
)这样每个注册的任务都会被自动包裹一层 defer func() { if r := recover(); r != nil { ... } }()。
立即学习“go语言免费学习笔记(深入)”;
-
cron.Recover()只捕获 panic,不处理返回的error;业务错误仍需主动判断和记录 - 如果你用了自定义 logger,传进去即可,比如
cron.Recover(myLogger) - 不要在
AddFunc的闭包里再套一层 recover——重复包裹没坏处但没必要,反而干扰调试
time.Ticker 场景下 recover 必须放在 goroutine 内部 defer 中
很多人把 recover() 写在启动 time.Ticker 的函数外面,比如:
func startJob() {
defer func() { recover() }() // ❌ 这个 recover 接不住 ticker 里的 panic
ticker := time.NewTicker(10 * time.Second)
go func() {
for range ticker.C {
doWork() // 这里 panic,外层 defer 根本看不到
}
}()
}Go 中 recover() 只对**同 goroutine** 中的 panic 有效。正确位置必须是 ticker 循环内部的 defer:
go func() {
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[job] panic at %s: %v", t.Format(time.RFC3339), r)
}
}()
doWork()
}()
}
}
}()- 每次 tick 都新建一个匿名函数并立即执行,确保 defer 绑定到当前 tick 的执行上下文
- 避免把 recover 放在 for 外层——那样只能捕获第一次 panic,之后 goroutine 就退出了
- 如果
doWork()执行时间超过 tick 间隔,考虑加 context 超时或 skip 逻辑,否则任务会堆积
容易被忽略的时区与秒级支持陷阱
即使加了 recover,任务也可能根本没注册成功,原因常藏在初始化参数里:
- 没传
cron.WithLocation(time.Local)→ 表达式按 UTC 解析,“每天 0 点”实际在北京时间上午 8 点跑 - 写了 6 段 cron 表达式(如
"* * * * * *")但没传cron.WithSeconds()→ v3 静默失败,AddFunc返回 error 但你没检查,任务压根没进调度器 - 容器环境硬编码
time.LoadLocation("Asia/Shanghai")→ 常因缺失 tzdata 导致 panic,time.Local更可靠
验证方法:注册后立刻调用 c.Entries()[0].Schedule.Next(time.Now()),打印返回时间,看小时是否符合预期。


















