应将 time.Ticker 抽象为接口并作为依赖注入,避免在函数内直接调用 time.NewTicker;否则测试会因真实时间等待而变慢、不可控,甚至死锁。

用 time.AfterFunc 或 time.NewTicker 模拟时钟会阻塞测试
直接在测试里启动真实 time.Ticker,会导致测试等待实际时间间隔(比如 5 秒),既慢又不可控。更糟的是,如果业务逻辑里用了 select + <ticker.c></ticker.c> 且没设超时,测试可能永远卡住。
根本原因:Go 的定时器是基于系统时钟的,无法被测试代码“快进”或“暂停”。必须把时间依赖抽出来,让测试能注入可控的时钟行为。
把 time.Ticker 抽成接口并传入依赖
不要在函数内部直接调用 time.NewTicker,而是接收一个符合 interface{ C 的参数(或更明确的自定义接口)。这样测试时可用 fake ticker 替代:
示例结构:
立即学习“go语言免费学习笔记(深入)”;
// 定义可替换的 ticker 接口
type Ticker interface {
C() <-chan time.Time
Stop()
}
<p>// 生产代码使用
func RunWithTicker(ticker Ticker, handler func()) {
for range ticker.C() {
handler()
}
}</p><p>// 测试时用
type FakeTicker struct {
ch chan time.Time
}
func (f <em>FakeTicker) C() <-chan time.Time { return f.ch }
func (f </em>FakeTicker) Stop() {}
测试中手动发送信号到 f.ch,就能精确控制“tick”发生的时机和次数。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
用 github.com/benbjohnson/clock 简化时间控制
第三方库 clock 提供了 clock.Clock 接口和 clock.NewMockClock(),它返回的 Ticker 可以手动推进时间:
常见操作:
- 调用
mockClock.Add(5 * time.Second)触发所有已注册的 ticker tick -
mockClock.AfterFunc(...)返回的 timer 也会响应Add - 避免自己手写 channel 控制逻辑,减少出错可能
注意:必须确保业务代码所有时间相关调用(time.Now、time.Sleep、time.After)都通过注入的 clock.Clock 实例调用,否则 mock 无效。
测试并发 ticker 逻辑时别漏掉 Stop() 和 goroutine 泄漏
真实 time.Ticker 不调用 Stop() 会持续发送时间信号,导致 goroutine 泄漏——测试跑完后 goroutine 还在运行,下次测试可能收到残留 tick。
测试中务必:
- 在
defer ticker.Stop()(如果是真实 ticker)或关闭 fake channel - 用
runtime.NumGoroutine()在测试前后断言 goroutine 数量是否恢复 - 对有循环
select的逻辑,确保退出路径能真正终止循环(比如通过donechannel 配合case )
最容易被忽略的是:fake ticker 的 channel 没关闭,而业务代码用 for range ticker.C() 死等,测试结束时 goroutine 卡在 range 上。

















