用 time.Ticker 仅适合单机低频场景,易堆积、无恢复、多实例重复;推荐 go-co-op/gocron(原生 context 支持)或 robfig/cron/v3(兼容 cron 表达式),关键需外接持久化与分布式锁。

用 time.Ticker 实现简单周期任务,但别直接上生产
多数人写第一个 Go 定时任务时会用 time.NewTicker 配合 for range 循环,代码简洁、逻辑直白。但它只适合单机、低频、不关心错失/重试的场景——比如每 5 秒打个日志。
容易踩的坑:
-
time.Ticker不感知任务执行耗时,若某次任务耗时 6 秒,下一次仍会在第 10 秒触发,导致“堆积”或并发执行 - 程序崩溃或重启后,任务完全丢失,无持久化、无恢复机制
- 无法跨进程协调,多实例部署时会重复执行
示例中常见错误写法:
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
go doWork() // 忘加 error 处理和 panic 捕获,goroutine 泄漏风险高
}选 robfig/cron/v3 还是 go-co-op/gocron?看调度精度和上下文需求
robfig/cron/v3(即经典 cron 库)支持 Unix cron 表达式、@every、@hourly 等语法,适合运维习惯强、需兼容已有 cron 规则的场景;但默认不支持传入 context.Context,超时控制和取消依赖手动封装。
立即学习“go语言免费学习笔记(深入)”;
go-co-op/gocron 原生支持 context.Context、链式调用、任务标签、立即执行(StartImmediately())、失败重试,更适合现代 Go 工程实践。
关键差异点:
- 若需秒级精度(如
*/3 * * * * *),两个库都支持,但gocron默认启用秒级模式,cron/v3需显式传入cron.WithSeconds() -
gocron的Do(func(ctx context.Context){})可自然承接 cancel 信号;cron/v3的func()签名固定,需额外包装 - 两者都不自带存储,多实例竞争问题仍需外接分布式锁(如 Redis + Lua)
如何避免定时任务在服务重启时漏跑?靠外部持久化 + 启动补偿
Go 程序本身无状态,gocron 或 cron/v3 的内存调度器在进程退出时就清空了。要保证“至少一次”,必须把任务元信息(下次应执行时间、状态、重试次数)存到外部存储。
实操建议:
- 用 SQLite 或 PostgreSQL 存一张
scheduled_jobs表,字段至少含:job_name、next_run_at(UTC 时间戳)、status(pending/running/success/failed) - 服务启动时,查出所有
next_run_at 且status = pending的任务,逐个触发并更新状态 - 任务执行成功后,立刻计算下一次
next_run_at并更新数据库,而非依赖调度器“自动推进”
注意:不要在补偿逻辑里直接 go job.Run(),需统一走调度器的 Start() 或 Do() 流程,否则脱离重试、限流等控制。
分布式环境下怎么防止多个实例同时执行同一任务?Redis 锁不是万能的
用 Redis 实现分布式锁(如 SET job:send_email NX EX 30)是最常见做法,但要注意三个硬伤:
- 锁过期时间难预估:任务可能运行超时,锁被别的实例抢走,造成并发执行
- Redis 主从异步复制,主节点写入锁后宕机,从节点升主,旧锁未同步,出现双写
- 没做锁续期(renew),长任务大概率被误释放
更稳妥的做法:
- 用 Redis + Lua 脚本实现原子性“检查+设置+续期”,例如
redis.SetNX配合redis.Pexpire在一个 pipeline 中完成 - 任务执行前先尝试获取锁,成功后再查数据库确认该任务仍为
pending状态(防缓存穿透) - 关键任务建议加幂等键(如
task_id + date作唯一索引),即使偶发重复也业务无害
真正复杂的调度需求(如依赖编排、失败告警、可视化面板),别硬扛——这时候该切到 Temporal 或 Argo Workflows,Go client 支持良好,但学习成本和基础设施投入明显上升。










