推荐用 github.com/robfig/cron/v3 实现准生产级定时任务,它支持秒级精度、时区隔离、并发控制;务必显式指定 time.Location,任务函数须无参无返回值,超时默认跳过,动态增删需 channel 串行管理,失败重试需手动封装。

用 github.com/robfig/cron/v3 实现准生产级定时任务
直接上手推荐用 cron/v3,它支持秒级精度、时区隔离、任务并发控制,且 API 清晰。别用老版本 v1 或 v2,它们不支持 Location 参数,本地时间错乱是常态。
常见错误:启动后任务没执行,或执行时间比预期晚 1 分钟——大概率是没传 *time.Location,默认用 UTC:
// ✅ 正确:显式指定上海时区
loc, _ := time.LoadLocation("Asia/Shanghai")
c := cron.New(cron.WithLocation(loc))
// ❌ 错误:用默认 UTC,导致北京时间任务延后 8 小时
c := cron.New()
- 任务函数必须无参数、无返回值,否则注册会 panic;需传参请闭包封装或用结构体方法绑定
- 若任务执行耗时超过调度间隔(比如每 10s 执行,但某次跑了 15s),
v3默认跳过下一次,避免堆积;如需并发运行,加cron.WithChain(cron.Recover(cron.DefaultLogger), cron.DelayIfStillRunning(cron.DefaultLogger)) - 注意
cron.New()不自动启动,记得调c.Start(),且程序退出前应c.Stop()防 goroutine 泄漏
Web 接口动态增删 Cron 任务要绕开全局单例
Web 管理后台常需要“新增一个每天凌晨 2 点发邮件的任务”,但 cron.Cron 实例不是线程安全的,不能在 HTTP handler 里直接 c.AddFunc(...) 后立刻生效——可能因并发写入 panic,或新加任务未被调度器感知。
正确做法是用 channel + 单 goroutine 统一管理:
立即学习“go语言免费学习笔记(深入)”;
type TaskOp struct {
Action string // "add", "remove"
Spec string
Func func()
Name string
}
var taskCh = make(chan TaskOp, 100)
// 启动专用调度管理 goroutine
go func() {
c := cron.New()
c.Start()
defer c.Stop()
for op := range taskCh {
switch op.Action {
case "add":
c.AddFunc(op.Spec, op.Func)
case "remove":
c.Remove(c.EntryID(op.Name))
}
}}()
- 所有 Web 接口操作都只往
taskCh发送指令,由单一 goroutine 串行处理,避免竞态 - 任务名(
Name)建议用唯一标识(如 UUID 或 hash(Spec+Func)),方便后续 remove - 不要在 handler 中调
c.Stop()再c.Start()重载——会清空全部任务,且中间有调度空窗
任务失败不重试?得自己加日志和恢复逻辑
cron/v3 本身不提供失败重试、告警、持久化。如果某个发短信任务因网络超时失败,它不会自动再试,也不会记录错误到数据库。
必须手动包装任务逻辑:
func wrapWithRetry(f func(), maxRetries int) func() {
return func() {
for i := 0; i <= maxRetries; i++ {
defer func() {
if r := recover(); r != nil {
log.Printf("task panic: %v", r)
}
}()
if err := runWithTimeout(f, 30*time.Second); err != nil {
if i == maxRetries {
alertOnFailure(err) // 自定义告警
log.Printf("task failed after %d retries: %v", maxRetries, err)
} else {
time.Sleep(time.Second * 2)
}
} else {
return
}
}
}
}
-
runWithTimeout需用context.WithTimeout控制单次执行上限,防止卡死阻塞整个调度器 - panic 捕获必须在 wrapper 内做,
cron.WithChain(cron.Recover(...))只能捕获顶层 panic,无法覆盖闭包内错误 - 关键任务建议把执行状态(开始/成功/失败/耗时)写入 DB,便于后台查询和补发
部署时注意容器时区与 Cron 表达式语义差异
Docker 默认用 UTC,但你的 0 0 2 * * *(秒级格式)本意是“每天 2:00 北京时间”,若容器没配时区,实际在 UTC 2:00(即北京时间 10:00)触发。
- 镜像构建时加
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime,或启动容器加-v /etc/localtime:/etc/localtime:ro - 表达式格式必须匹配所用库:
cron/v3默认是Seconds Minutes Hours DayOfMonth Month DayOfWeek六字段(支持秒),而系统 crond 是五字段;混用会导致解析错位 - Kubernetes 中若用
Job替代应用内调度,注意startingDeadlineSeconds和concurrencyPolicy的行为和 Go 应用内调度完全不同,别混为一谈
真正麻烦的从来不是怎么加一行 c.AddFunc,而是任务生命周期里的可观测性、故障转移和跨环境一致性——这些得靠你填,库不会替你做。










