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

t.Cleanup 是子测试清理的唯一可靠选择
当你在 t.Run 里启动临时服务、创建临时文件或监听端口,就必须用 t.Cleanup,而不是 defer。因为 defer 属于外层函数作用域,等整个测试函数返回才执行;而子测试早已结束,资源可能被下一个子测试复用甚至冲突——比如端口被占、临时目录被误删。
- 常见错误现象:
defer os.RemoveAll(tempDir)写在t.Run里,结果所有子测试共用一个tempDir,或删掉别人刚建的目录 -
t.Cleanup绑定到当前*testing.T生命周期:子测试一结束(无论 pass/fail/panic),它就立刻执行 - 它不支持返回
error,也不接受context控制超时——出错只能t.Log,不能中断测试流程
闭包变量捕获是高频翻车点
在循环中注册 t.Cleanup 时,若直接引用循环变量,最后所有清理函数看到的都是循环末尾的值。这不是 Go 特性,是闭包语义本身的问题,但测试里特别容易忽略。
- 错误写法:
for _, name := range names { t.Cleanup(func(){ os.Remove(name) }) }→ 全部删最后一个name - 正确写法:
for _, name := range names { name := name; t.Cleanup(func(){ os.Remove(name) }) } - 或者显式传参:
t.Cleanup(func(n string){ os.Remove(n) }(name))
什么该进 t.Cleanup,什么不该
t.Cleanup 不是万能垃圾桶,它的价值在于「外部状态隔离」。纯内存操作、结构体重置这类事,直接写在子测试末尾更清晰,也更容易调试。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须用:
httptest.NewUnstartedServer().Start()后配t.Cleanup(srv.Close)、os.CreateTemp后配t.Cleanup(os.RemoveAll) - 没必要用:清空局部
map、重置slice、赋值字段——这些不会污染其他测试,也不依赖 OS 状态 - 信号提示:一个测试里注册 5+ 个
t.Cleanup,大概率说明逻辑耦合太重,该拆分或封装 setup/teardown
兼容性和替代方案
t.Cleanup 是 Go 1.14+ 才支持的特性。如果你还在维护老版本代码,不能直接升级,就得靠作用域控制 + defer 模拟。
立即学习“go语言免费学习笔记(深入)”;
- Go < 1.14:把子测试逻辑提成独立函数,参数传入
*testing.T和资源句柄,在函数末尾用defer清理 -
TestMain仍是全局资源(如数据库连接池、mock server)的唯一正解,t.Cleanup只管单个测试粒度 - 性能上没负担,但滥用会模糊测试意图——真正难的不是调哪个函数,而是判断「这个资源的生命周期到底该归属到哪一层测试」
t.Run 的嵌套深度,比背规则管用。

















