MiddleCodeOccasionalNot answered yet
What does this goroutine loop print on Go 1.22+ vs Go ≤1.21?
Predict what this program prints, separately for Go 1.22+ and Go ≤1.21, and explain why the two differ. Note: the goroutines run concurrently, so account for the order of the printed values, not just their values.
func main() {
var wg sync.WaitGroup
for _, v := range []int{1, 2, 3} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Print(v, " ")
}()
}
wg.Wait()
}
Predict the output.
On Go 1.22+ each iteration gets its own v, so the goroutines print 1 2 3 in some order. On Go ≤1.21 they all shared one v mutated by the loop and typically printed 3 3 3. The portable fix is v := v inside the loop or passing v as an argument.
- ✗Believing
go func(){...}()copies the loop variable at call time — it captures the variable, not its value - ✗Assuming the
3 3 3behavior is a data race rather than a deterministic capture of one shared variable - ✗Thinking Go 1.22 scoping also reorders the goroutines into
1 2 3— the order is still unspecified
- →How does the Go 1.22 change scope the loop variable, and does it cost an allocation per iteration?
- →Why does passing
vas a function argument fix the bug on every Go version?
Contents
What does this code print?
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for _, v := range []int{1, 2, 3} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Print(v, " ")
}()
}
wg.Wait()
}
Output
Go 1.22+: 1 2 3 (in some order)
Go ≤1.21: 3 3 3 (typically)
Why
The closure go func(){ ... }() captures the variable v, not its value at launch time.
- Go ≤1.21: there is one loop variable
vfor the whole loop. By the time the scheduler runs the goroutines the loop has finished andvholds its last value3, so all three goroutines print3. - Go 1.22+: the language creates a fresh
veach iteration, so every goroutine captures its own copy and prints1,2,3. The order is still unspecified — goroutines are scheduled independently.
⚠️ This is the single most common Go interview gotcha. The portable fix that works on every version:
for _, v := range []int{1, 2, 3} {
v := v // shadow: a fresh variable per iteration
go func() { fmt.Print(v, " ") }()
}
// or pass as an argument: go func(v int){ ... }(v)Contents