Go

How did loop variable semantics change in Go 1.22, and what does this print?

Question 35HardGo 1.22 to 1.25
var fns []func()
for i := 0; i < 3; i++ {
	fns = append(fns, func() { fmt.Print(i, " ") })
}
for _, f := range fns {
	f()
}

var ptrs []*int
for i := range 3 { // range over an int, Go 1.22
	ptrs = append(ptrs, &i)
}
fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2])

Output with Go 1.22 or later (when go.mod says go 1.22 or higher):

0 1 2 0 1 2

Before Go 1.22, both for forms had one loop variable for the whole loop. Every closure and every pointer referred to that same variable, so the first loop printed 3 3 3. Range-over-int did not exist before 1.22 (it needs go 1.22 or later in go.mod), but the equivalent old form for i := range []int{0, 1, 2} printed 2 2 2 for the pointer version. That was the most common Go bug, especially with goroutines started inside loops. Since Go 1.22, each iteration gets a new variable. In a three-clause loop, the value is copied into the next iteration's variable before the post statement runs, so changing i inside the body still affects how the loop progresses.

Points to raise:

  • The behavior depends on the go line in the module's go.mod, not on the compiler version you have installed.
  • Workarounds like i := i are no longer needed.
  • If a pointer to the loop variable escapes, each iteration may now cost an allocation. Usually this is negligible.
  • for i := range n loops from 0 to n-1. The type of i matches the type of n.
  • go vet's loopclosure check is less relevant for newer modules.

More on Language Fundamentals & Types

All 39 Language Fundamentals & Types questions