time.ticker适合秒级轻量轮询,但必须用select+default非阻塞读取并启动goroutine执行任务,避免tick堆积;首次延迟需手动sleep,且务必defer ticker.stop()防泄漏。

直接用 time.Ticker 启动 goroutine 轮询是最常用也最容易出错的方式
它适合秒级、毫秒级的轻量轮询,比如健康检查、缓存刷新、指标采集。但别把它当成“定时器回调”来用——time.Ticker 只负责准时发信号,不关心你有没有处理完。
常见错误是写成这样:
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
doSomething() // 耗时 1.5s?下一次 tick 立刻就来了,直接并发执行
}
后果很直接:任务堆积、goroutine 泄漏、CPU 毛刺、下游被压垮。正确做法是把消费逻辑完全移出主循环,用独立 goroutine 处理每个信号:
- 必须用
go func() { ... }()包裹实际工作,避免阻塞ticker.C通道 - 每个工作 goroutine 内部要加
defer+recover(),防止单个 panic 带崩整个轮询流 - 务必在退出前调用
ticker.Stop(),否则 goroutine 和 channel 会一直活着 - 如果任务涉及 HTTP 或 DB,
http.Client和*sql.DB必须全局复用,不能在回调里新建
time.AfterFunc 递归调用不是重复调度的解法
有人想“执行完再注册下一次”,于是写:
func schedule() {
time.AfterFunc(5*time.Second, func() {
doWork()
schedule() // ❌ 错误:没有退出控制,无法 Stop,context 无法传递,goroutine 不可控
})
}
这会导致:schedule() 每次都新起 goroutine,服务重启时旧 goroutine 还在跑;没 context 控制,没法优雅取消;超时、重试、限流全得自己补。
真正需要单次延迟触发才用 time.AfterFunc,重复调度请无条件选 time.Ticker 或第三方库。
动态 URL 列表轮询必须避免共享切片竞态
当你要轮询的地址列表会运行时增删(比如从配置中心拉取),直接遍历一个全局 []string 是危险的:
- 多个 goroutine 同时读写这个切片,
go run -race会立刻报 data race - 轮询 goroutine 正在
for _, u := range urls,另一个 goroutine 执行append(urls, newURL),可能 panic 或漏掉新 URL
安全做法是用 channel 通信代替共享内存:
- 定义
add chan string接收新增 URL - 用
urls []string存当前快照,每次轮询前做一次 copy(或用sync.RWMutex保护读写) - 加
quit chan struct{}实现优雅退出,ticker.Stop()和 channel close 都要配对
高吞吐场景下 time.Ticker 必须和 worker pool 解耦
每秒触发上千次?别让 ticker.C 直接连到业务逻辑。robfig/cron/v3 在这种场景下会卡死,因为它所有任务都在同一个 goroutine 里串行执行。
生产级方案是三层结构:
-
time.Ticker只干一件事:准时往inbound chan Task里塞任务 -
inbound设缓冲(如make(chan Task, 1024)),满时主动丢弃或降级,不阻塞 ticker - 固定数量的 worker goroutine 从
inbound消费,每个内部带context.WithTimeout和错误恢复
关键点在于:触发频率和执行能力彻底分离。哪怕某个任务 hang 住,也不会影响下一次 tick 发射,更不会拖垮其他任务。
最容易被忽略的是监控缓冲区水位——len(inbound) / cap(inbound) > 0.75 就该告警,说明消费者跟不上了,而不是盲目加大 buffer。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











