time.Ticker需显式Stop防goroutine泄漏;cron/v3注意时区与DelayIfStillRunning防重入;gocron仅限单机内存调度;定时任务必设超时context并监控goroutine数。

用 time.Ticker 做简单周期任务,但别让它泄漏 goroutine
如果你只需要每 5 秒查一次数据库、每分钟 ping 一次服务,time.Ticker 足够轻量。但它不会自动停止,一旦启动就一直跑,容易在服务重启或配置变更时堆积 goroutine。
- 必须显式调用
ticker.Stop(),通常放在defer或上下文取消时 - 避免在 for-select 循环里直接写业务逻辑,先检查
ctx.Done()再处理 tick - 不要把
time.Tick()(底层无 Stop 接口)用于长期运行的服务
ticker := time.NewTicker(30 * time.Second) defer ticker.Stop()for { select { case <-ctx.Done(): return case <-ticker.C: doWork() } }
用 github.com/robfig/cron/v3 解析 crontab 表达式,注意时区和并发控制
标准库不支持类似 0 0 * * * 这种表达式,cron/v3 是事实标准,但默认使用本地时区,且每个任务单独起 goroutine —— 如果任务执行慢,可能堆积多个并发实例。
- 初始化时传入
cron.WithLocation(time.UTC)避免因服务器时区导致误触发 - 用
cron.WithChain(cron.Recover(cron.DefaultLogger), cron.DelayIfStillRunning(cron.DefaultLogger))防止重入 -
DelayIfStillRunning比SkipIfStillRunning更安全:前者排队,后者直接丢弃
用 gocron 做内存级多任务调度,但别把它当分布式方案
gocron 适合单机多定时任务管理,API 直观,支持链式注册、立即运行、手动暂停/恢复。但它所有状态都在内存里,进程一挂,所有未触发任务就丢了。
- 任务函数必须是无状态的,或自行处理幂等(比如加数据库唯一约束)
- 如果需要“至少执行一次”,得配合外部存储(如 Redis + Lua 记录 last_run_at)
- 不要用它做跨机器协同 —— 它没有节点发现、选举或任务分片能力
遇到 context deadline exceeded 和任务卡死,先查底层阻塞点
定时任务看似简单,但实际常因 I/O 阻塞、锁竞争或 context 传递不完整导致整个调度器僵住。典型现象是日志停更、新任务不再触发,但进程还在。
立即学习“go语言免费学习笔记(深入)”;
- 所有 HTTP 请求、数据库查询、文件读写,都必须带超时 context:
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) - 避免在定时任务里用
log.Printf替代结构化 logger,高频率打日志可能阻塞输出缓冲区 - 用
runtime.NumGoroutine()定期采样,若持续上涨,大概率是某个任务没正确释放资源
真正难的不是怎么设间隔,而是让每次触发都可预期、可追踪、可中断。很多线上事故,都始于一个没设超时的 http.Get 调用,卡住了整个 cron.Entry 的 goroutine。










