Go 测试中 ticker 未停止为何让 goroutine 泄漏
来源:17golang原创
时间:2026-10-14 22:43:22 210浏览 收藏
Go 测试里看到 goroutine 数量持续上涨时,问题经常被归咎于“忘了调用 ticker.Stop()”。更准确的判断是:Stop() 只停止后续 tick,不会关闭 ticker.C;如果测试启动的 worker 一直阻塞在这个通道上,它仍然没有退出路径,泄漏的其实是 goroutine 生命周期。
time.NewTicker、ticker.C消费者和测试 cleanup 是三个不同的生命周期。- 不要用
for range ticker.C等待测试结束,要在select中增加done或context.Done()。 - 关闭时同时执行
Stop、发送退出信号并等待 worker 返回,单独 Stop 不能证明没有泄漏。
先把 ticker、通道和 goroutine 的生命周期分开
Ticker 负责按周期向 C 发送时间值,worker goroutine 负责消费这些值,测试则负责在断言完成后通知 worker 收尾。三者混在一个函数里时,最容易把“停止发消息”误认为“结束消费方”。
| 对象 | 停止方式 | 不能替代什么 |
|---|---|---|
*time.Ticker | Stop() | 不会关闭 C |
ticker.C | 由消费者选择退出分支 | 不会自动发“结束”值 |
| worker goroutine | done、context 或其他取消信号 | 不能靠 ticker 停止自行返回 |
| 测试 | cleanup 后等待 worker | 不能只看断言已经通过 |
下面这种写法在测试结束时只调用了 Stop。tick 不再产生,但消费循环没有结束条件:
func startRefresh() *time.Ticker {
ticker := time.NewTicker(20 * time.Millisecond)
go func() {
// 这里的循环只等待 tick,没有测试退出分支。
for range ticker.C {
refresh()
}
}()
return ticker
}
func TestRefresh(t *testing.T) {
ticker := startRefresh()
// Stop 只停止后续 tick,不能让上面的 for range 结束。
t.Cleanup(ticker.Stop)
}

测试退出时要同时完成三件事
可收尾的实现要给 worker 一个明确的退出信号,再用等待点确认它真的返回。WaitGroup 不是为了修复 ticker,而是把“测试已经发出关闭请求”和“worker 已经离开”这两个时刻分开。
type poller struct {
ticker *time.Ticker
done chan struct{}
wg sync.WaitGroup
}
func newPoller() *poller {
p := &poller{
ticker: time.NewTicker(20 * time.Millisecond),
done: make(chan struct{}),
}
p.wg.Add(1)
go func() {
defer p.wg.Done()
for {
select {
case
真实代码中要保证 Close 只调用一次;如果对象可能被多个清理路径触发,可以再用 sync.Once 包住关闭逻辑。若 refresh() 本身会阻塞,也要让它接收 context,否则 worker 虽然离开了 select,仍可能卡在业务调用里。

Stop 不能等同于关闭通道
官方 time.Ticker 文档明确说明,Stop 后不会再发送 tick,但不会关闭通道。这一设计避免并发读取者误把“通道关闭”当成错误 tick,也允许后续复用 ticker。于是,for range ticker.C 在这里没有自然终点。
另外,Go 1.23 起垃圾回收器可以回收已经不可达、尚未停止的 ticker。这个变化减少了某些 timer 对象的长期保留,但不能替代 worker 的退出协议:只要 goroutine 仍持有 ticker.C 并等待输入,它就不是一个已经结束的 goroutine。测试清理依然应该显式停止定时工作并等待消费者。
用现象对应证据的清单排查泄漏
| 现象 | 优先检查 | 修复方向 |
|---|---|---|
| Stop 后测试进程仍有后台 worker | worker 是否只监听 ticker.C | 增加 done 或 context.Done() |
| 重复运行测试后 goroutine 数上涨 | cleanup 是否等待 worker | 用 WaitGroup.Wait() 建立收尾屏障 |
| 关闭偶发卡住 | 业务处理是否阻塞、Close 是否重复调用 | 让处理可取消,并用 sync.Once 保证幂等 |
排查时不要只把 ticker.Stop() 加到测试末尾就结束。先画出谁创建 ticker、谁持有 channel、谁负责通知退出,再确认测试结束前 worker 已经返回;这比单看 goroutine 数量更容易找到根因。
相关问题
调用 ticker.Stop 后为什么不能从 ticker.C 读到结束信号?
因为 Stop 不关闭通道,也不会发送额外的结束值。需要结束消费者时,应使用独立的 done 通道或 context。
测试里写 defer ticker.Stop 还需要 done 吗?
如果没有其他 goroutine 消费 ticker.C,Stop 通常足够停止定时工作;一旦启动了长期读取的 worker,就必须提供它能选择到的退出分支。
WaitGroup 能单独解决 ticker 泄漏吗?
不能。WaitGroup 只能等待,不能发出取消信号。正确组合是先让 worker 能收到 done 或 context,再用 WaitGroup 确认它已经退出。
-
495 收藏
-
Golang · Go问答 | 19分钟前 | CGO · 内存管理 · Go问答 · Go CGO unsafe.Pointer 指针传递 runtime/cgo.Handle runtime.Pinner124 收藏
-
119 收藏
-
401 收藏
-
335 收藏
-
217 收藏
-
191 收藏
-
246 收藏
-
235 收藏
-
189 收藏
-
Golang · Go问答 | 38分钟前 | 标准库 · 文件读取 · Go问答 · Go archive/zip Store DEFLATE File.Open FileHeader.Method File.OpenRaw241 收藏
-
211 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习