t.cleanup是子测试粒度唯一可靠的清理机制,绑定*testing.t生命周期,子测试退出(无论pass/fail/panic)即逆序执行;defer属函数作用域,须等外层test函数返回才执行,易致资源冲突。

t.Cleanup 是测试结束时唯一能按子测试粒度可靠执行的扫尾机制,不是“失败才清理”,也不是“函数返回就执行”,它绑定 *testing.T 生命周期,子测试一退出(无论 pass/fail/panic)就触发,且按注册逆序执行。
为什么 t.Cleanup 不能替代 defer?
defer 属于函数作用域,整个 TestXxx 函数返回才执行;而 t.Cleanup 绑定的是单个 *testing.T 实例 —— 子测试 t.Run("sub1", ...) 结束后立刻清理,不会等外层测试函数跑完。
- 错误现象:
defer os.RemoveAll(tmpDir)写在t.Run里,结果所有子测试共用一个tmpDir,或删掉下一个子测试刚建的目录 - 正确做法:每个子测试内调用
t.TempDir()+t.Cleanup(func(){ os.RemoveAll(...) }) - 关键区别:defer 是栈式延迟,t.Cleanup 是测试对象生命周期钩子
t.Cleanup 闭包变量捕获的坑怎么避?
在循环中注册 t.Cleanup 时,若直接引用循环变量,所有清理函数最终看到的都是最后一次迭代的值 —— 这是 Go 闭包语义本身的问题,不是测试框架 bug。
- 错误写法:
for _, name := range names { t.Cleanup(func() { os.Remove(name) }) }→ 全部删最后一个name - 正确写法一:
for _, name := range names { name := name; t.Cleanup(func() { os.Remove(name) }) } - 正确写法二:
for _, name := range names { t.Cleanup(func(n string) { os.Remove(n) }(name)) }
哪些资源必须进 t.Cleanup,哪些不必?
t.Cleanup 的价值在于隔离外部状态。它不处理纯内存操作,只管可能污染其他测试或 OS 级资源的释放。
- 必须用:
httptest.NewUnstartedServer().Start()后配t.Cleanup(srv.Close)、os.CreateTemp()后配t.Cleanup(os.RemoveAll)、启动 mock DB 实例后配t.Cleanup(db.Close) - 不必用:清空局部
map、重置slice、赋值结构体字段 —— 这些不跨测试生效,也不依赖系统句柄 - 信号提示:一个测试里注册超过 5 个
t.Cleanup,大概率说明逻辑耦合太重,该拆分或封装 setup/teardown
真正难的不是写不写 t.Cleanup,而是判断「这个资源的生命周期到底该归属到哪一层测试」——比如端口监听,是整个包共用一个,还是每个子测试独占一个?这个问题没想清,清理逻辑再漂亮也容易翻车。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











