What is a classic closure-over-loop-variable bug in Go, and how was it fixed in Go 1.22?
Quick Answer
Before Go 1.22, a for loop reused the same variable across every iteration, so a closure (like a goroutine literal) capturing the loop variable by reference would see whatever value the variable held when the closure actually ran, not the value at the time the closure was created — usually the loop's final value, since the closure typically runs after the loop has moved on. The classic symptom: for _, v := range items { go func() { fmt.Println(v) }() } printing the same (often last) value multiple times instead of each item. Go 1.22 changed loop semantics so each iteration gets its own fresh copy of the loop variable, which fixes this bug by default for code compiled with go 1.22 or later in go.mod — but the bug is still very real, and very commonly asked about, for anyone maintaining code on older Go versions or reasoning about how the bug happened historically.
Detailed Answer
This is probably the single most famous Go gotcha, common enough that the language itself changed to eliminate it by default.
The bug, pre-Go 1.22
items := []string{"a", "b", "c"}
var wg sync.WaitGroup
for _, v := range items {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v) // BUG (pre-1.22): all three goroutines may print "c"
}()
}
wg.Wait()
All three closures captured a reference to the same v variable, which the loop kept reassigning. By the time the goroutines actually ran, the loop had likely already finished, leaving v at its final value, "c", for every closure to see.
The traditional fix (still valid, still seen in real code)
for _, v := range items {
v := v // shadow: creates a new variable per iteration
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v)
}()
}
Or, equivalently, pass v as a parameter to the closure:
go func(v string) {
defer wg.Done()
fmt.Println(v)
}(v)
The Go 1.22 fix
Starting with Go 1.22 (and only for modules whose go.mod declares go 1.22 or later), the language spec changed so each loop iteration gets its own copy of the loop variable automatically. The exact same original code, with no v := v shadow and no parameter trick, now prints "a", "b", "c" correctly.
Why this still matters in interviews
Plenty of production code still targets pre-1.22 semantics, or was written before the change and never revisited. Understanding why the bug happened, plus both fixes, is exactly the kind of "gotcha" question that comes up constantly in Go interviews. That's true no matter which Go version a specific codebase targets today.