Go t.Parallel() Loop-Variable Failures - Fix Parallel Tests in CI
Table-driven subtests that call t.Parallel() all observe the same (usually last) case, because the parallel closures captured a shared loop variable. Under older loop-variable semantics every subtest reads the variable after the loop finished.
What this error means
A parallel table test fails or asserts against the wrong case - every subtest behaves as if it got the final iteration’s data. It can also pass by luck, making it intermittently wrong rather than reliably failing.
--- FAIL: TestCases/case-a (0.00s)
cases_test.go:25: got result for "case-z", want "case-a"
# all parallel subtests saw the last loop valueCommon causes
Loop variable captured by a parallel closure (pre-1.22)
Before Go 1.22, the range variable was shared across iterations. A t.Parallel() subtest runs after the loop advanced, so its closure reads the final value.
Building with an older go directive
A module whose go directive is below 1.22 keeps the old shared-variable semantics even on a newer toolchain, so the bug persists.
How to fix it
Bind the case to a local variable
Shadow the loop variable inside the loop so each subtest closes over its own copy.
for _, tc := range cases {
tc := tc // pin per iteration (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// use tc...
})
}Raise the go directive to 1.22+
Go 1.22 changed loop semantics so each iteration gets a fresh variable, removing the need to re-bind.
go mod edit -go=1.22
go test ./...How to prevent it
- Re-bind range variables before
t.Parallel()on pre-1.22 modules. - Set the
godirective to 1.22+ to get per-iteration loop variables. - Run
go test -shuffle=onand the race detector to surface capture bugs.