t.cleanup是子测试清理的唯一可靠选择,因其绑定当前*testing.t生命周期,子测试结束即执行;defer属外层函数作用域,易致资源冲突或误删。

t.Cleanup 是 Go 测试中唯一能保证「子测试结束即执行」的清理机制,不是可选项,而是必须用在临时 OS 资源上的硬性要求。
为什么 defer 在 t.Run 里会出问题
在 t.Run 内部用 defer 清理资源(比如删临时目录、关 mock server),会导致所有子测试共享同一套延迟逻辑,等整个外层测试函数返回才统一执行——此时子测试早已结束,资源可能被复用或冲突。
常见错误现象:
- 端口被下一个子测试尝试监听时提示
address already in use - 多个子测试共用一个
os.TempDir(),defer os.RemoveAll()删掉别人刚建的文件 - mock HTTP server 被提前关闭,后续子测试发请求直接失败
根本原因:defer 属于函数作用域,不感知子测试生命周期;而 t.Cleanup 绑定到当前 *testing.T 实例,子测试一退出就触发。
t.Cleanup 必须传无参无返回值函数
注册清理函数时,t.Cleanup 只接受 func() 类型。闭包捕获变量是高频翻车点,尤其在循环中。
错误写法:
for _, name := range names {
t.Cleanup(func() {
os.Remove(name) // 所有 closure 都引用同一个 name 变量,最终全删最后一个
})
}
正确写法(任选其一):
- 显式声明局部变量:
name := name; t.Cleanup(func() { os.Remove(name) }) - 用立即执行函数传参:
t.Cleanup(func(n string) { os.Remove(n) }(name))
注意:t.Cleanup 调用本身不会 panic,但注册的函数若 panic,只会记录 cleanup failed 日志,不影响原测试结果判定。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
哪些资源必须用 t.Cleanup,哪些不必
t.Cleanup 的核心价值是隔离「外部状态」。它不是用来清空局部变量或重置结构体字段的。
必须用(否则可能污染其他测试):
-
httptest.NewUnstartedServer().Start()后配t.Cleanup(srv.Close) -
os.CreateTemp或t.TempDir()后配t.Cleanup(os.RemoveAll) -
gomock.NewController(t)—— GoMock 自动检测并注册ctrl.Finish(),不用手写defer
不必用(纯内存操作,无副作用):
-
map清空、slice重置、struct 字段赋零值 - 关闭已关闭的 channel、重复关闭
io.Closer(应由资源创建方负责)
信号提示:单个测试里注册超过 5 个 t.Cleanup,大概率说明 setup 过于耦合,该拆成独立 helper 函数或封装为 test fixture。
Go
t.Cleanup 是 Go 1.14+ 特性。老版本无法升级时,不能靠 TestMain 替代子测试粒度的清理——它只适合全局一次性的资源(如启动一个 PostgreSQL 容器)。
可行替代方案:
- 把子测试逻辑提成独立函数,参数传入
*testing.T和资源句柄,在函数末尾用defer清理 - 用
sync.Once+ 包级变量模拟单次初始化/销毁,但要小心并发和测试顺序依赖
真正难的从来不是怎么调 t.Cleanup,而是判断「这个资源的生命周期到底该归属到哪一层测试」——比如一个内存数据库实例,是每个子测试独占,还是整个测试函数共享?这点错,后面全错。










