Go testing.T Cleanup 怎么安排资源释放:注册顺序、子测试与失败路径
来源:17golang原创
时间:2026-08-26 00:21:06 461浏览 收藏
CI 里最难受的一类 Go 单测失败,不是业务断言错了,而是前一个测试留下的临时目录、环境变量或共享连接,让后一个测试偶尔读到旧状态。单个测试在本机能过,换成 `t.Parallel()` 或跑完整包就开始抖,通常说明资源的生命周期没有跟测试作用域绑定。
- 函数内的
defer适合清理当前函数创建的资源,t.Cleanup更适合把释放动作注册给测试作用域。 - 同一个测试中多次注册的清理函数按后进先出执行,子测试的清理不会越过父测试边界。
- 清理函数要能处理断言失败、提前返回和子测试失败;不要把关键回收动作藏在“成功路径”里。
先看一个会在完整测试里抖动的场景
假设测试需要一个临时配置目录。开发者先写了一个辅助函数,把目录建好后返回路径;调用方在测试末尾手动删除。只要中间新增一个断言,或者某个分支提前 return,目录就可能留在机器上。更隐蔽的是,下一次测试为了省事复用了同一个环境变量。
func TestLoadConfig(t *testing.T) {
dir := t.TempDir()
t.Setenv("APP_CONFIG_DIR", dir)
if err := writeConfig(dir); err != nil {
t.Fatal(err)
}
// 这里失败时,后续手动清理代码根本不会执行。
got := loadConfig()
if got.Port != 8080 {
t.Fatalf("port = %d, want 8080", got.Port)
}
}
这段代码本身并不一定有错:t.TempDir 和 t.Setenv 都会由测试框架回收。问题在于,很多项目里的自定义资源夹具仍然沿用“返回资源,调用者记得清理”的约定。清理动作越靠近创建动作,越不容易被新增分支漏掉。
defer、Cleanup 和框架托管资源怎么选
三种方式不是互相替代。我的判断规则是:资源只在当前函数里使用,就用 defer;资源由测试辅助函数创建,并且应该在当前测试结束时释放,就在创建函数里注册 t.Cleanup;临时目录、环境变量这类标准能力优先交给 testing 包。
| 场景 | 优先方式 | 验收重点 |
|---|---|---|
| 当前函数打开的文件 | defer file.Close() | 打开成功后立即注册 |
| 测试夹具创建的监听器或连接 | t.Cleanup | 夹具失败也不能遗留进程 |
| 临时目录和环境变量 | t.TempDir、t.Setenv | 不要在并行测试中手动改全局状态 |
Cleanup 的价值不在于“比 defer 更晚”,而在于它把清理动作挂到了 *testing.T 的生命周期上。辅助函数可以创建资源、注册清理,然后把一个已经可用的句柄交回测试主体。
把创建和清理绑在一个测试夹具里
下面这个夹具创建一个带关闭动作的临时存储。注册清理要紧跟在创建成功之后,避免中间又插入一段可能失败的初始化。
type testStore struct {
root string
db *sql.DB
}
func newTestStore(t *testing.T) *testStore {
t.Helper()
root := t.TempDir()
db, err := sql.Open("sqlite", filepath.Join(root, "test.db"))
if err != nil {
t.Fatalf("open test db: %v", err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("close test db: %v", err)
}
})
store := &testStore{root: root, db: db}
if err := migrate(store.db); err != nil {
t.Fatalf("migrate: %v", err)
}
return store
}
这里有两个细节。第一,t.Helper() 让失败位置落在测试调用处,排查时不必穿过夹具内部。第二,关闭数据库的动作在迁移之前就已经注册,所以迁移失败时依然会执行关闭。清理函数里用 t.Errorf 记录关闭错误即可,不要再次调用会中断流程的 t.Fatal。
清理注册顺序决定了依赖资源的关闭顺序
多个清理动作会按后进先出执行。可以把它理解成栈:后创建的资源先释放。若 HTTP 客户端依赖一个临时服务,那么先注册服务的关闭,再创建客户端并注册客户端关闭,最终顺序就是客户端先关、服务后关。

func newFixture(t *testing.T) *fixture {
t.Helper()
service := startService(t)
t.Cleanup(func() { service.Close() })
client := newClient(service.URL())
t.Cleanup(func() { client.CloseIdleConnections() })
return &fixture{service: service, client: client}
}
如果把服务关闭注册在最后,测试结束时可能先关服务,再让客户端清理连接,日志里就会出现一串看似无关的连接错误。清理函数的注册顺序应该表达资源依赖,而不是按代码长度或“最后统一处理”的习惯排列。
子测试的边界:父资源能共享,子清理不能越界
父测试注册的清理会等所有子测试结束后执行;子测试注册的清理只在该子测试返回后执行。这很适合“父级建一次、子级复用”的只读夹具,但不适合让子测试修改一个必须恢复的全局变量。
func TestParser(t *testing.T) {
store := newTestStore(t)
t.Run("valid", func(t *testing.T) {
seed(store, "valid")
t.Cleanup(func() { clearRows(store) })
checkValid(t, store)
})
t.Run("invalid", func(t *testing.T) {
seed(store, "invalid")
t.Cleanup(func() { clearRows(store) })
checkInvalid(t, store)
})
}
这个例子仍有一个并行风险:两个子测试如果改成 t.Parallel(),它们会同时操作同一个存储。Cleanup 只保证清理时机,不会自动提供隔离。需要并行时,把夹具创建移进每个子测试,或者为每个子测试分配独立 schema、目录和连接。
失败路径要怎么验收
不要只验证成功断言。可以故意让初始化、主体断言和子测试分别失败,然后观察临时进程、文件描述符和数据库连接是否都收尾。实际项目里,我会把检查拆成三条:
- 夹具中途失败:创建完成的资源是否已有清理登记。
- 测试调用
t.Fatal或 panic:清理是否仍然执行。 - 父子测试组合:子测试资源是否在父级资源仍可用时释放。
如果清理函数本身要调用业务代码,最好让它具备幂等性。比如关闭已经关闭的连接、删除不存在的临时文件,都不应该再制造一条会掩盖原始断言的错误。

常见问题
Cleanup 和 defer 哪个先执行?
如果都注册在同一个测试函数里,函数返回时函数体中的 defer 会先执行,测试作用域结束时才执行 t.Cleanup。不要依赖两者之间传递仍然有效的资源,最好让每个清理动作独立完成。
Cleanup 里能不能调用 t.Fatal?
不建议。清理阶段应尽量使用 t.Errorf 或记录错误,避免在回收过程中再次中断测试。更重要的失败原因应该在主体测试阶段已经留下。
用了 t.TempDir 还需要手动删除目录吗?
不需要。t.TempDir 会把目录生命周期交给测试框架。只有额外创建的目录、外部服务或自定义缓存,才需要在夹具中补上自己的 t.Cleanup。
一份可以直接放进代码审查的清单
- 资源创建成功后,是否立刻注册了清理动作?
- 清理动作是否覆盖提前返回、断言失败和初始化失败?
- 多个资源之间的注册顺序,是否对应“后创建先释放”?
- 子测试是否共享了不该共享的目录、连接或全局变量?
- 清理错误是否会掩盖真正的业务断言?
把生命周期写进夹具,测试主体就只需要表达“准备什么、验证什么”。这不是为了让代码看起来更整齐,而是让失败之后仍然留下可信的现场:该关的连接已经关,该删的目录已经删,下一条测试不会继承上一条测试的情绪。
-
377 收藏
-
275 收藏
-
467 收藏
-
485 收藏
-
411 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习