A 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()) // 3The 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.