Sunday, August 16, 2026
Why Did My Closure Capture the Wrong Loop Variable?
Posted by

Why Did My Closure Capture the Wrong Loop Variable? (Go 1.22 Changed This)
Because the closure holds a reference to the loop variable, not a copy of the number printed at that iteration. Before Go 1.22, for reused one variable. After the loop, every stored func or goroutine read the last value. Go 1.22 creates a fresh variable per iteration, but only if the module's go line is 1.22 or later.
The failure
This prints 3 3 3 on Go 1.21 (and on 1.22+ if go.mod still says go 1.21).
package main
import "fmt"
func main() {
var prints []func()
for i := 1; i <= 3; i++ {
prints = append(prints, func() { fmt.Println(i) })
}
for _, fn := range prints {
fn()
}
}
Goroutines do the same:
values := []string{"a", "b", "c"}
for _, v := range values {
go func() { fmt.Println(v) }()
}
You usually get c c c. t.Parallel() inside a range over cases used to hide odd failures: the loop finished first, then every subtest saw the last v.
Taking &v or passing &v into a helper is the same trap: one address, last contents.
Why this happens
A closure does not snapshot. It names the same storage the loop writes. Pre-1.22, that storage was declared once. Each iteration assigned a new value into it. Anything that ran after the iteration, a goroutine, a defer, a slice of funcs, saw whatever sat there at call time.
Go 1.22 changed the spec: each iteration gets its own i / v. The old examples print 1 2 3 and a b c. The switch is gated by go 1.22 in go.mod, not merely by the toolchain version. A 1.23 compiler still uses the old rule for a module that says go 1.21.
Variables you declare inside the body were always per-iteration. The change is only for names the for statement itself declares.
The fix
On go 1.22+, write the obvious loop. The compiler does the per-iteration copy.
On older modules, or when you must be obvious, shadow or pass by value:
for _, v := range values {
v := v
go func() { fmt.Println(v) }()
}
for _, v := range values {
go func(val string) { fmt.Println(val) }(v)
}
Before bumping go 1.22, run GOEXPERIMENT=loopvar go test ./... (Go 1.21). Tests that used t.Parallel() with a captured loop var often start failing, they were never testing the cases you thought.
Keep v := v if a reviewer will read the code on mixed versions. It is harmless under the new rule.
Learn more: