This is a different trap from the loop-variable bug below — Go 1.22 doesn't save you here, because the variable in question isn't the loop variable at all. You build up a slice of closures inside a loop, each one meant to remember a running total at the point it was created. Every one of them, when called later, reports the exact same number: whatever the total happened to be after the loop finished.
var funcs []func() int
total := 0
items := []int{10, 20, 30}
for _, v := range items {
total += v
funcs = append(funcs, func() int { return total }) // captures 'total' itself, not its value
}
for _, f := range funcs {
fmt.Println(f()) // NOT 10, 30, 60 -- all three print the FINAL total
}
// 60
// 60
// 60A closure is a function literal that references a variable declared outside its own body. Go closures capture that variable by reference, not by value — meaning if the outer variable changes after the closure is created, the closure sees the new value, not a frozen snapshot from when it was defined.
A closure capturing shared state
A common use is a function that returns another function, which keeps its own private state across calls via the captured variable:
func makeCounter() func() int {
count := 0
return func() int { // this closure captures 'count' by reference
count++
return count
}
}
c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2 — count persisted between calls
fmt.Println(c()) // 3makeCounter above wants exactly this behavior: every call to c() sees count's latest value, on purpose. The callout at the top of this page is the same reference-capture mechanism used by accident — each closure in the slice captures the one total variable, not a value, so asking any of them for "the total at this point in the loop" doesn't work; they all answer with the same final number. The fix is to give each iteration its own variable to capture, which is unrelated to the loop-variable change below since total was never the loop variable in the first place:
var funcs []func() int
total := 0
items := []int{10, 20, 30}
for _, v := range items {
total += v
snapshot := total // a fresh variable each iteration, holding today's value
funcs = append(funcs, func() int { return snapshot })
}
for _, f := range funcs {
fmt.Println(f()) // 10, 30, 60 -- each closure has its own snapshot
}The classic loop-variable gotcha (fixed in Go 1.22)
Before Go 1.22 (released February 2024), a for loop reused a single variable across every iteration, so closures created inside the loop — especially goroutines launched per iteration — all captured the SAME variable, which usually held its final value by the time they ran. Go 1.22 changed the language so each iteration gets its own fresh copy of the loop variable, eliminating an entire category of bugs and the x := x workaround that was previously idiomatic.
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Go 1.22+: prints 0, 1, 2 (each iteration, own i)
}() // before Go 1.22: often printed 3, 3, 3
}Why this matters for code written before 2024
Any Go codebase or tutorial predating Go 1.22 may still contain the manual workaround, explicitly shadowing the loop variable inside the loop body (i := i) before using it in a closure. That workaround is now unnecessary but harmless — it's a useful signal for reading older Go code, and safe to remove once a project's minimum Go version is 1.22 or later.