Go 测试中 ticker 未停止为何让 goroutine 泄漏
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 确认它已经退出。
Go JSON 数字解码到 int 为什么会溢出
- 上一篇
- Go JSON 数字解码到 int 为什么会溢出
- 下一篇
- Go time.Parse 为什么使用 UTC 而不是本地时区
-
- Golang · Go问答 | 19分钟前 |
- Go plugin 加载时版本不匹配怎样判断
- 495浏览 收藏
-
- Golang · Go问答 | 19分钟前 | CGO · 内存管理 · Go问答 · Go CGO unsafe.Pointer 指针传递 runtime/cgo.Handle runtime.Pinner
- Go cgo 传指针到 C 怎样遵守指针规则
- 124浏览 收藏
-
- Golang · Go问答 | 21分钟前 |
- Go SQL 事务遇到死锁怎样设计有限重试
- 119浏览 收藏
-
- Golang · Go问答 | 25分钟前 |
- Go database/sql Rows 关闭后继续 Next 会怎样
- 401浏览 收藏
-
- Golang · Go问答 | 27分钟前 | go · 二进制 · encoding/binary binary.Read
- Go binary.Read 怎样处理结构体对齐误解
- 335浏览 收藏
-
- Golang · Go问答 | 28分钟前 |
- Go database/sql Rows 遇到 Scan 错误怎样清理
- 217浏览 收藏
-
- Golang · Go问答 | 28分钟前 |
- Go hex 解码奇数长度字符串为什么失败
- 191浏览 收藏
-
- Golang · Go问答 | 33分钟前 |
- Go x509 CertPool 怎样追加 PEM 根证书
- 246浏览 收藏
-
- Golang · Go问答 | 33分钟前 |
- Go base64 解码遇到非法字符怎样定位位置
- 235浏览 收藏
-
- Golang · Go问答 | 35分钟前 | go · TLS · tls Go 自签名证书 crypto/x509 RootCAs
- Go TLS 客户端为何不自动信任自签名根证书
- 189浏览 收藏
-
- Golang · Go问答 | 38分钟前 | 标准库 · 文件读取 · Go问答 · Go archive/zip Store DEFLATE File.Open FileHeader.Method File.OpenRaw
- Go archive/zip FileHeader Method 怎样区分存储和压缩条目
- 241浏览 收藏
-
- Golang · Go问答 | 41分钟前 |
- Go gzip 读取多成员文件为什么结果不同
- 211浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 474次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 531次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 551次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 503次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 323次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- 一篇文章带你搞懂Go语言标准库Time
- 2022-12-22 298浏览
-
- Go time包AddDate使用解惑实例详解
- 2023-01-11 164浏览
-
- Go Ticker 周期性定时器用法及实现原理详解
- 2023-01-07 417浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
