表驱动测试配合 t.Parallel 的子测试,是 Go 1.22 之前最经典的循环变量陷阱:所有子测试看到的都是最后一个用例。这篇用同一份代码、只改 go.mod 的 go 版本复现它,并验证子测试名的转义规则、-run 怎么选子测试、-parallel 如何限制并行度。
[go.mod 写 go 1.21] 退出码 1
子测试 TestCapture/one 里看到的 tc.name 却是 "three"(它测的是别人的用例)
子测试 TestCapture/two 里看到的 tc.name 却是 "three"(它测的是别人的用例)
--- FAIL: TestCapture (0.00s)
--- PASS: TestCapture/three (0.01s)
--- FAIL: TestCapture/one (0.01s)
--- FAIL: TestCapture/two (0.01s)
FAIL
FAIL lab 0.112s
[go.mod 写 go 1.22] 退出码 0
--- PASS: TestCapture (0.00s)
--- PASS: TestCapture/two (0.01s)
--- PASS: TestCapture/one (0.01s)
--- PASS: TestCapture/three (0.01s)
PASS
ok lab 0.119s子测试里调用了 t.Parallel() 就会暂停,等外层循环跑完才继续,这时共享的循环变量 tc 已经是最后一个用例。go.mod 写 go 1.21 时,"one" 和 "two" 两个子测试都看到了 tc.name == "three",而且因为 tc.in 与 tc.want 同属最后一个用例,用它们互相比较是检查不出来的——必须拿子测试的名字之类的独立信息来对照。
x_test.go:32: 子测试名: TestNames/a_b x_test.go:32: 子测试名: TestNames/a_b#01 x_test.go:32: 子测试名: TestNames/x/y x_test.go:32: 子测试名: TestNames/#00 x_test.go:32: 子测试名: TestNames/UPPER x_test.go:32: 子测试名: TestNames/tab_here --- PASS: TestNames (0.00s) --- PASS: TestNames/a_b (0.00s) --- PASS: TestNames/a_b#01 (0.00s) --- PASS: TestNames/x/y (0.00s) --- PASS: TestNames/#00 (0.00s) --- PASS: TestNames/UPPER (0.00s) --- PASS: TestNames/tab_here (0.00s)
规范规定每次迭代有自己独立的变量。同一份代码在 go 1.22 的模块里全部通过。注意这是按模块的 go 版本决定的:Go 1.27.1 工具链编译 go 1.21 的模块仍然是旧语义,老项目升级工具链后不改 go.mod 不会自动获得新语义。
go test -run "TestNames/a_b" 运行了:TestNames, TestNames/a_b, TestNames/a_b#01 go test -run "TestNames/^a_b#01$" 运行了:TestNames, TestNames/a_b#01
空格变成下划线,重名加 #01,空字符串是 #00,斜杠不转义(x/y 会被当作层级),制表符也变成下划线。-run 用斜杠分隔每一层,每层都是正则,所以 TestNames/a_b 匹配到两个,要精确选择就加 ^ 和 $。
-parallel=1:同时在跑的并行子测试最多 1 个,总用时 0.356 s -parallel=2:同时在跑的并行子测试最多 2 个,总用时 0.245 s -parallel=4:同时在跑的并行子测试最多 4 个,总用时 0.168 s -parallel=8:同时在跑的并行子测试最多 8 个,总用时 0.134 s
-parallel=N 限制同时运行的并行测试数,实验里同时在跑的子测试最多正好是 N(1、2、4、8)。总用时从 0.356 s 降到 0.134 s(含测试二进制启动的约 0.1 秒)。
go_spec.html:Each iteration has its own separate declared variable (or variables) [Go 1.22]. The variable used by the first iteration is declared by the init statement. The variable used by each subsequent iteration is declared implicitly before executing the post statement and initialized to the value of the previous iteration's variable at Run runs f as a subtest of t called name. It runs f in a separate goroutine and blocks until f returns or calls t.Parallel to become a parallel test. Run reports whether f succeeded (or at least did not fail before calling t.Parallel). Run may be called simultaneously from multiple goroutines, but all such calls must return before the outer test function for t returns.
语言规范明确写着 “Each iteration has its own separate declared variable”,并标注 [Go 1.22]。t.Run 的文档说明子测试在单独的 goroutine 里运行,调用 t.Parallel 之前它阻塞外层。