该用 t.cleanup 而不是 defer 的情况是:当测试函数中存在多个子测试(t.run)且每个子测试需独立清理资源时,t.cleanup 按子测试生命周期自动执行,而 defer 延迟到整个测试函数结束,易导致资源泄漏或复用错误。

什么时候该用 t.Cleanup 而不是 defer
当测试函数里有多个子测试(t.Run),且每个子测试都需要独立清理时,t.Cleanup 是唯一靠谱的选择。defer 会等到整个测试函数返回才执行,而子测试早已结束,资源可能被提前复用或泄漏。
-
t.Cleanup绑定到当前测试生命周期,子测试结束时自动触发,哪怕它 panic 或提前 return -
defer只属于外层函数作用域,对t.Run内部的清理完全“看不见” - 常见错误现象:
defer os.RemoveAll(tempDir)在子测试里写,结果所有子测试共用一个tempDir,或删错目录
t.Cleanup 的执行顺序和陷阱
它按注册顺序**逆序**执行,这点和 defer 一样,但容易忽略的是:它不捕获闭包变量的实时值。
- 如果在循环里注册
t.Cleanup,又用了循环变量(比如for _, name := range names { t.Cleanup(func(){... name ...}) }),最后所有清理函数看到的都是循环末尾的name值 - 正确做法是显式传参或用局部变量捕获:
name := name; t.Cleanup(func(){ os.Remove(name) }) - 它不能返回 error,也不支持 context 控制超时——清理逻辑本身出错就只能 log,没法中断测试流程
文件/进程类资源清理必须用 t.Cleanup
临时文件、监听端口、启动的 mock server 这类外部状态,一旦漏清理,轻则影响后续测试,重则导致 CI 随机失败。
- 示例:启动一个本地 HTTP server 测试 API,
srv := httptest.NewUnstartedServer(...); srv.Start()后必须配t.Cleanup(srv.Close) - 用
defer srv.Close()看似简洁,但在t.Run("case1", ...)里注册,实际等整个测试函数结束才关,下一个子测试可能因端口被占而失败 - 注意兼容性:
t.Cleanup是 Go 1.14+ 才支持,老版本只能靠defer+ 手动管理作用域
性能开销和过度使用的信号
t.Cleanup 本身几乎没有运行时成本,但滥用会模糊测试意图,增加维护负担。
立即学习“go语言免费学习笔记(深入)”;
- 纯内存操作(比如重置 map、清空 slice)没必要进
t.Cleanup,直接写在子测试末尾更清晰 - 如果一个测试里注册了 5+ 个
t.Cleanup,大概率说明逻辑耦合太重,该拆成多个独立测试或封装 setup/teardown 函数 - 它不替代
TestMain:全局初始化(如数据库连接池)仍该放TestMain,t.Cleanup只管单个测试粒度
t.Run 的嵌套深度,比背规则管用。










