t.Run 是 Go 官方唯一支持的子测试分组机制,创建独立生命周期的嵌套测试作用域;必须用其返回的 *testing.T 断言,子测试名需唯一、机器友好、避免空格和动态拼接,并注意循环变量捕获陷阱。

用 t.Run 做子测试分组,本质是创建嵌套的测试作用域
Go 的 testing.T 本身不支持“测试套件”或“测试类”,t.Run 是唯一官方支持的逻辑分组机制。它不是语法糖,而是真实启动一个子测试——有独立的生命周期、可单独运行、失败不阻断同级其他子测试。
常见错误是把 t.Run 当作普通函数调用,忽略其返回的 *testing.T 必须被用于后续断言:直接在闭包外用原始 t 会导致并发竞争或 panic。
- 子测试名必须唯一,否则
go test -run=TestFoo/xxx无法精准匹配 - 名字里避免空格和斜杠以外的特殊字符,否则
-run参数解析可能出错 - 不要在
t.Run外部提前 return,否则子测试不会执行 - 如果子测试里调用了
t.Parallel(),父测试也必须先调用t.Parallel()
参数名和结构体字段命名不一致时,t.Run 名字怎么写才好定位
子测试名本质是调试线索,不是描述性文案。推荐用 "field:expected_value" 或 "case:input_A_output_B" 这类机器友好格式,方便 grep 和 CI 日志过滤。
比如验证 JSON 解码时字段映射是否正确,别写 "解码用户名字段",而写 "Username:empty_string" 或 "User.Name:''" —— 后者能直接对应代码里的 User.Name 字段,失败时一眼看出是哪个 case 崩了。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 优先用结构体字段名 + 实际值(如
"ID:123"),而不是自然语言 - 值过长时截断并加
...,例如"Body:{"name":"a...} - 避免动态拼接含空格的名字,比如
t.Run("input is "+s, ...),容易导致-run匹配失效
为什么 t.Run 里不能直接用外部变量做断言判断
闭包捕获变量时,如果循环中反复调用 t.Run,但没显式传参或复制值,会所有子测试共享最后一次迭代的变量值——这是 Go 循环变量复用的经典陷阱。
比如遍历 []string{"a", "b"} 并对每个值跑子测试,若直接在闭包里用 v,两个子测试实际都看到 "b"。
- 正确做法:把变量作为参数传进闭包,或在循环体内用
val := v显式复制 - 更安全的是把测试数据定义为结构体切片,每个元素带 name 和 input/output,再 range 它
- 用
go vet能检测部分这类问题,但不是 100% 覆盖
子测试太多时,go test -v 输出太长,怎么快速定位失败项
-v 下每个子测试都会打印一行 === RUN TestXxx/yyy 和 --- PASS 或 --- FAIL,但失败信息混在中间,肉眼难扫。真正高效的方式是结合 -run 和 -failfast。
- 先用
go test -v -run=TestXxx看全量输出,找到失败的子测试名(比如TestParse/Invalid_JSON) - 再精确重跑:
go test -v -run=TestParse/Invalid_JSON,专注看那个 case - 加
-failfast可让第一个子测试失败就停,省得等全部跑完 - CI 中建议默认加
-json,用工具解析结构化输出,比文本 grepping 更可靠
子测试真正的复杂点不在写法,而在命名策略和数据隔离——名字决定你查日志花 5 秒还是 5 分钟,变量捕获方式决定你 debug 是看一眼就懂,还是怀疑人生半小时。

















