time.Ticker 不调用 Stop() 必然导致 goroutine 泄漏,因其内部 goroutine 持续向无缓冲 channel ticker.C 发送时间戳且永不退出;正确做法是用 select + 退出通道替代 for range,并在启动处 defer ticker.Stop()。

time.Ticker 为什么一用就泄漏 goroutine
不调用 ticker.Stop() 是最常见、最隐蔽的泄漏源头。time.Ticker 内部会启动一个长期运行的 goroutine,持续向 ticker.C(无缓冲 channel)发送时间戳;一旦你没显式 Stop,这个 goroutine 就永远不会退出,哪怕主逻辑早已结束。
典型错误写法:for range ticker.C —— 它看似“自动遍历”,实则只是语法糖,既不感知 context 取消,也不处理 panic,更不会主动关闭 channel。一旦任务执行超时或 panic,循环提前退出,但后台 goroutine 还在拼命尝试发送,ticker.C 满了就丢,goroutine 却照常存活。
现象:pprof 查 /debug/pprof/goroutine 看到大量 timerproc,内存缓慢上涨,CPU 出现毛刺。
修复要点:
立即学习“go语言免费学习笔记(深入)”;
- 必须用
select替代for range,至少带一个退出通道(如ctx.Done()) - 在 goroutine 启动处加
defer ticker.Stop(),确保生命周期可控 - 别把
ticker存全局变量——多协程读写易竞态;用闭包捕获或结构体字段封装更安全
如何让每次 tick 都真正并发执行,而不是排队等上一次结束
核心陷阱:把任务逻辑直接写在 case 分支里同步执行。一旦 <code>task() 耗时超过 tick 间隔(比如间隔 2 秒,任务跑了 3 秒),下个 tick 到来时 ticker.C 已满,新时间戳被直接丢弃——这不是 bug,是 Go 的设计选择:宁可跳过,也不堆积。
正确做法是在 case 分支中立即启动新 goroutine:
case t := <-ticker.C:
go func(t time.Time) {
task(t)
}(t)
但注意两个关键点:
- 若任务依赖循环变量(如
for i := range jobs),必须显式传参:go func(id int) { ... }(i),否则所有 goroutine 共享终值 - 无限制启 goroutine 是生产环境高频雷区:500ms 一次 tick、跑 1 小时,可能累积上万 goroutine。用带缓冲 channel 控制并发数,例如
sem := make(chan struct{}, 5),每次执行前先sem ,执行完立刻 <code><-sem
多个周期任务共存时,能不能共用一个 ticker
不能。不同周期的任务(比如“每 3 秒发心跳”和“每 30 秒刷缓存”)如果塞进同一个 ticker.C,就得在内部做时间判断、状态维护、独立启停控制——逻辑耦合、难以测试、无法单独关闭或调整间隔。
Go 的优势是 goroutine 便宜,每个任务该有自己的 time.Ticker 和专属 goroutine:
- 各自管理生命周期:
defer ticker.Stop()放在对应 goroutine 内部 - 关闭统一走
context.Context,比如ctx, cancel := context.WithCancel(context.Background()),任务内select监听ctx.Done() - 避免用全局变量传递 ticker 或 channel;用结构体字段封装或闭包捕获更利于单元测试
什么时候该放弃 time.Ticker,换 cron 库
time.Ticker 是内存级、单机、无状态的——进程挂了任务就丢,节点扩容后可能重复执行,失败了没法自动重试。这些不是缺陷,而是边界。
需要以下能力时,time.Ticker 就不够用了:
- 任务需持久化(重启后继续执行)
- 需分布式协调(避免多实例重复触发)
- 需失败重试、告警、人工干预入口
- 需秒级精度表达式(如
"*/5 * * * * *")或复杂调度逻辑(如“每月最后一个周五”)
此时推荐:
-
github.com/robfig/cron/v3:事实标准,支持WithSeconds()开启秒级,但默认不打日志,出错静默 -
github.com/go-co-op/gocron:API 更现代,支持 async job、链式调用、.WithLogger()插拔,对 HTTP/DB 资源复用也更友好
真正容易被忽略的是:cron 表达式别硬编码在代码里。热重载靠外部驱动——存在 etcd / Redis / 文件中,用 fsnotify 监听变化,或定期轮询读取。否则改个时间就得发版。



















