用time.ticker实现定时任务需异步执行+防重入:启动goroutine处理耗时逻辑,配合sync.mutex或atomic.bool避免并发冲突;不可阻塞ticker.c循环,须防goroutine泄漏与tick丢失。

用 time.Ticker 实现简单定时任务,但别直接裸用
Go 标准库没有内置 cron,time.Ticker 适合固定间隔(如每 5 秒执行一次),但它只管“等时间”,不处理“执行逻辑是否超时”或“上一次没跑完怎么办”。
常见错误是把耗时操作塞进 Ticker.C 的 for 循环里,导致后续 tick 被阻塞、任务堆积:
for range ticker.C {
doHeavyWork() // ❌ 可能阻塞下一次 tick
}
正确做法是启动 goroutine 异步执行,同时加简单防重入保护(比如用 sync.Mutex 或原子标志):
- 用
select+default避免 goroutine 泛滥(防止前序任务卡住时疯狂启新协程) - 记录上次执行时间,跳过明显延迟的 tick(比如预期 10s 一次,但已延迟 30s,就跳过)
-
time.Ticker不支持动态修改间隔,改间隔必须 stop + 新建
用 robfig/cron/v3 做类 Unix cron 表达式调度
这是 Go 生态最稳定的第三方 cron 库,支持秒级精度(v3 默认支持 6 字段,如 "0 0 * * * ?")、时区、任务 panic 捕获和日志钩子。
注意几个关键配置点:
- 初始化时传
cron.WithSeconds()才能解析带秒的表达式;默认不启用秒字段,"* * * * *"是分钟级 - 用
cron.WithChain(cron.Recover(cron.DefaultLogger))自动 recover panic,否则单个 panic 会让整个调度器停摆 -
cron.New()返回的实例不是 goroutine 安全的,添加/移除 job 后需显式调用Start()(如果还没启)或不用额外操作(已运行中会自动生效) - job 函数签名必须是
func(),不能带参数——需要闭包捕获或提前绑定
避免在 cron job 中直接操作全局变量或共享状态
多个 cron job 并发执行时,如果都读写同一个 map 或结构体字段,极易触发 data race。Go 的 go run -race 能发现,但线上可能漏掉。
典型场景:统计指标计数器、缓存刷新、DB 连接池健康检查。建议:
- 用
sync.Map替代原生map做高频读写的共享状态 - 对 DB 操作,确保每个 job 使用独立的
*sql.DB连接(标准库已做连接池管理,不用自己锁) - 如果 job 必须串行执行(比如操作同一份文件),不要靠“cron 错开时间”来规避,而应加分布式锁或本地互斥锁(
sync.Mutex) - 避免在 job 里直接调用
log.Printf—— 多个 goroutine 同时打日志可能混行;用结构化日志库(如zerolog)并绑定 job 名称上下文
生产环境必须处理的三个冷门但致命问题
本地测试跑得稳,上线后出问题往往卡在这几处:
-
cron默认使用time.Local时区,K8s Pod 或 Docker 容器里可能没设TZ环境变量,导致实际按 UTC 跑 —— 显式传cron.WithLocation(time.UTC)或time.LoadLocation("Asia/Shanghai") - 程序收到
SIGTERM时,cron不会自动Stop(),正在运行的 job 可能被粗暴中断(比如写一半的文件)。务必在os.Interrupt或syscall.SIGTERM处理中调用cron.Stop()并等待WaitForJobs() - 用
context.Context控制 job 超时(尤其 HTTP 请求、DB 查询),但cron本身不透传 context —— 需在 job 内部手动构造带 timeout 的 context,例如ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
真正麻烦的从来不是“怎么起一个定时任务”,而是“它在凌晨三点内存暴涨时,有没有安静退出”。











