必须显式指定时区,否则 Docker 或精简 Linux 环境中因 /etc/localtime 缺失或损坏导致 time.LoadLocation 失败而 panic;推荐生产环境使用 cron.New(cron.WithLocation(time.UTC))。

用 cron.New() 初始化时必须显式指定时区
直接调用 cron.New() 在 Docker 容器或精简 Linux 环境中大概率 panic “invalid time zone”,因为 cron/v3 默认依赖 time.Local,而该值需通过系统 /etc/localtime 加载——很多镜像里这个文件缺失或软链损坏。
解决方法只有两个靠谱选项:
-
cron.New(cron.WithLocation(time.UTC))—— 生产环境首选,避免时区歧义 - 确保容器安装
tzdata并设置ENV TZ=Asia/Shanghai,再用cron.WithLocation(time.LoadLocation("Asia/Shanghai"))
别信“本地跑得通,上线再改”的说法。UTC 是唯一跨环境稳定的基准。
cron.AddFunc 返回的 EntryID 必须持久保存
cron.AddFunc() 返回的 EntryID(int64 类型)是后续调用 cron.Remove() 的唯一凭证。它不是自增序列,也不可推导,丢了就真删不掉了。
立即学习“go语言免费学习笔记(深入)”;
常见错误写法:
- 在函数内临时接收:
id, _ := c.AddFunc(...); doSomething(); c.Remove(id)—— 作用域一结束,id就不可追溯 - 存在非并发安全的全局 map 或 slice,多 goroutine 写入时 panic
推荐做法:
- 用
sync.Map或map[cron.EntryID]*TaskMeta+sync.RWMutex存储 ID 到任务元信息的映射 - 元信息至少包含:描述、创建时间、原始 cron 表达式、关联资源(如 DB 连接池句柄)
cron.Remove() 不中断正在执行的 job
cron.Remove(id) 只是从调度队列中摘除该条目,已触发但尚未返回的 job 函数会继续运行到底。这不是 bug,是设计使然——强行中断可能破坏事务一致性或留下脏状态。
如果你的任务耗时较长(比如 HTTP 调用、大文件写入),必须自己处理取消逻辑:
- 在 job 内部使用
context.WithTimeout()或传入外部ctx - 所有阻塞操作(
http.Client.Do、os.WriteFile、db.QueryRow)都应支持上下文取消 - 不要在
cron.Remove(id)后立刻关闭相关 channel 或释放连接池,除非你确认 job 已退出
短任务可配合 sync.WaitGroup 等待当前轮次结束,但仅限可控场景;长任务必须靠 context 驱动。
热更新或服务重启前必须调用 cron.Stop()
*cron.Cron 启动后会起一个常驻 goroutine 监听时间通道。如果丢弃实例却不调 Stop(),那个 goroutine 就永远卡在 select { case 上,既不退出也不释放内存。
后果是:goroutine 数缓慢上涨,GC 压力增大,最终 OOM。尤其在频繁 reload 配置或灰度发布时极易暴露。
正确姿势:
- 全局只持有一个
*cron.Cron实例,挂载到应用生命周期管理器(如 Gin 的Server.RegisterOnShutdown) - 在服务关闭钩子中显式调用
c.Stop(),并等待几毫秒让 goroutine 退出(time.Sleep(10 * time.Millisecond)即可) - 别把
cron.New()和c.Start()写在 HTTP handler 里——每次请求都新建实例,等于批量制造泄漏点
真正难的从来不是“怎么加任务”,而是“怎么干净地收尾”。没显式 Stop() 的 cron 实例,就像没拔电源的电器,表面安静,暗处发热。


















