t.Run 是 Go 子测试的唯一入口,必须使用以实现逻辑隔离、命名过滤、并行执行和正确生命周期管理;直接循环断言会导致共享 *testing.T 实例、无法过滤、失败中断全部用例。

t.Run 是子测试的唯一入口,不是可选项
Go 没有内置的参数化测试语法,t.Run 就是构造逻辑隔离、命名清晰、可过滤可并行的子测试的强制手段。不调用 t.Run 直接在循环里写断言,会导致所有用例共享一个 *testing.T 实例——某个用例 t.Fatal 会直接终止整个测试函数,后续用例全跳过;go test -run=TestParse/invalid_json 这类子测试匹配也完全失效。
常见错误写法:
for _, tc := range tests {
if got := Parse(tc.input); got != tc.want {
t.Errorf("Parse(%q) = %v, want %v", tc.input, got, tc.want)
}
}
正确结构必须包含 t.Run:
for _, tc := range tests {
tc := tc // 防止闭包捕获循环变量
t.Run(tc.name, func(t *testing.T) {
got := Parse(tc.input)
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
-
tc := tc这一行不能省:Go 循环中tc是复用变量,不显式复制会导致所有子测试看到最后一个用例的值 - 子测试名(
tc.name)建议带语义,比如"empty_string"、"negative_number",方便定位失败点 - 每个子测试内可独立调用
t.Cleanup,适合关闭临时文件或恢复环境变量
加 t.Parallel() 前先确认无状态共享
子测试默认串行执行。t.Parallel() 能显著缩短总耗时,但前提是这些子测试之间**绝对不共享可变状态**:全局 map、未加锁的计数器、打开的文件句柄、复用的 http.Client、甚至同一个数据库连接池都可能引发竞态。
立即学习“go语言免费学习笔记(深入)”;
典型踩坑场景:
- 多个子测试并发调用
os.WriteFile("tmp.json", ...)→ 文件内容被覆盖或写入错乱 - 共用一个
sync.Map但没在每个子测试里重置 → 前一个子测试写入的数据影响后一个的断言 - 测试中启动了本地 HTTP server(如
httptest.NewServer)却没在t.Cleanup中关闭 → 端口占用导致后续子测试失败
安全做法:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 只对纯计算型、无 I/O、无外部依赖的子测试加
t.Parallel() - 涉及文件、网络、时间(
time.Now())、随机数(rand.Intn)的,一律禁用并行 - 不确定是否安全?先不加,用
go test -race跑一遍,再决定
子测试命名要能支撑 go test -run 过滤
t.Run 的第一个参数不只是日志标签,它构成完整的子测试路径,被 go test -run 解析。比如 t.Run("json/valid", ...) 和 t.Run("json/invalid", ...),就能用 go test -run=TestParse/json 一次性跑所有 json 相关子测试。
命名建议:
- 用斜杠分层:
"cache/hit"、"cache/miss"、"cache/expired" - 避免空格和特殊字符,shell 会解析失败
- 不要用动态生成的名字(如
fmt.Sprintf("test_%d", i)),否则无法稳定过滤
调试时直接指定子测试比注释掉其他用例快得多,这也是为什么命名要有信息量——你不想每次 grep 日志找 “哪个 invalid 出错了”,而是直接 go test -run=TestValidate/invalid_email。
别在子测试里 panic 或 os.Exit
t.Fatal 和 t.Fatalf 是子测试内终止自身的正确方式;panic 或 os.Exit 会绕过 testing 框架的清理流程,导致 t.Cleanup 不执行、临时资源不释放、甚至测试进程直接退出。
尤其注意以下情况:
- 调用第三方库函数返回
error后,别忽略它——要显式检查并用t.Fatal报告,否则测试看似通过实则逻辑未覆盖 - 使用
log.Fatal替代t.Fatal→ 测试提前退出,覆盖率统计中断,CI 可能误判为成功 - 在
t.Cleanup回调里调用t.Fatal→ 无效,Cleanup 内只能记录日志,不能触发失败
子测试的生命周期由 testing 包严格管理,任何跳出这个流程的操作都会让失败不可见、资源不可控、结果不可信。

















