time.Ticker 每到间隔就发信号,不等任务完成;直接 for range ticker.C 调用耗时函数会导致 goroutine 堆积 OOM;应主循环只调度,用带缓冲 channel(如 sem := make(chan struct{}, 5))限流并发数。

time.Ticker 会持续发信号,不等任务执行完就继续触发
这是最常被误读的一点:ticker.C 是一个无缓冲通道,每到间隔时间就往里塞一个 time.Time 值,完全不管上一次启动的 goroutine 是否还在运行。如果你在 for range ticker.C 里直接调用耗时函数,又没做并发控制,就会快速堆积大量 goroutine,最终 OOM。
正确做法是每次收到 tick 后,立即启一个新 goroutine 处理任务,主循环只负责“调度”,不阻塞:
go func() {
for t := range ticker.C {
go func(t time.Time) {
doWork()
}(t)
}
}()但要注意:这样会无限起 goroutine,没做任何限流或防重入。实际中必须加约束。
用带缓冲的 channel 控制最大并发数
限制同时运行的任务数,最轻量的方式是用带缓冲的 sem channel 当信号量:
立即学习“go语言免费学习笔记(深入)”;
-
sem := make(chan struct{}, 5)表示最多 5 个任务并发 - 每次想执行任务前先
select尝试写入sem,成功才起 goroutine - goroutine 结束前必须
归还令牌,否则后续任务永远卡住
关键代码片段:
for range ticker.C {
select {
case sem <- struct{}{}:
go func() {
defer func() { <-sem }()
doWork()
}()
default:
log.Println("skipped: too many tasks running")
}
}注意不能把 defer 放在 goroutine 外层,否则归还时机错乱;也不能漏掉 <code>defer,否则 channel 慢慢就满了。
任务停止必须 close(done) 或 send 到 stop channel
仅靠 ticker.Stop() 不足以让正在运行的 goroutine 退出。它只让 ticker.C 不再发送新时间,但已启动的 doWork() 还在跑,且 goroutine 本身不会自动结束。
必须配合一个显式的停止信号机制:
- 定义
done := make(chan struct{}) - 在 goroutine 内用
select监听done和ticker.C - 外部想停任务时,
close(done)或done
典型结构:
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
go func(t time.Time) { doWork() }(t)
}
}
}()不要用 for _ = range ticker.C + time.Sleep 模拟 ticker
有人为“避免 ticker 积压”而写这种代码:
for {
<-time.After(interval)
go doWork()
}这看似安全,实则更危险:它把周期变成了“上次任务完成 + interval”,一旦 doWork() 执行时间超过 interval,整个节奏就彻底漂移,而且无法感知 ticker 是否已被 Stop。
真正需要“任务完成后再等 interval”的场景,应该用 time.AfterFunc 链式调用,或手动 Reset timer,而不是绕开 time.Ticker 的设计本意。
真正难处理的从来不是 ticker 本身,而是你没想清楚:这个任务能不能并行?要不要跳过积压?失败了要不要重试?这些逻辑必须显式编码,不能指望 channel 自动帮你做决策。


















