Closures in Go: Functions That Remember
How closures capture variables, why those variables move to the heap, the := shadowing trap, and the Go 1.22 loop-variable change.
On this page
A closure is a function that remembers the variables from the place where it was created, and keeps using them even after that outer function has returned. Closures are how Go builds counters, generators, middleware and many configuration patterns without classes.
Nested functions can see outer variables
Start simple: a function value declared inside another function can read the outer variables.
func greet() {
name := "Sabbir"
display := func() {
fmt.Printf("Hi, %s\n", name) // reads name from the outer function
}
display()
}Returning a function that remembers
The interesting part begins when the inner function is returned and called later.
package main
import "fmt"
func counter() func() int {
count := 0
return func() int {
count++ // the same count variable, every call
return count
}
}
func main() {
c := counter()
fmt.Println(c(), c(), c()) // 1 2 3
d := counter()
fmt.Println(d(), d()) // 1 2 (a brand-new count)
}counter() has already returned, yet count is still alive and keeps its value between calls. Each call to counter() creates a new count, so c and d do not share state.
Where does the variable live? (heap vs stack)
Normally a local variable lives on the function's stack frame and disappears when the function returns. When the Go compiler sees that a returned closure still references count, its escape analysis moves count to the heap instead. The closure keeps a reference to it, and the garbage collector frees it once nothing references the closure anymore.
You can see the compiler's decision yourself:
go build -gcflags=-m .
# ./main.go:6:2: moved to heap: countThat is the "memorised on the heap" behaviour from my original notes: the closure closes over the variable, which is where the name comes from.
Shadowing: a subtle trap
Here is the example from the repository:
const a = 10
var p = 100
func outer() func() {
money := 100
show := func() {
money := money + a + p // := creates a NEW local money
fmt.Println("Money =", money)
}
return show
}
f := outer()
f() // Money = 210
f() // Money = 210It prints 210 every time, not 210, 320, 430. Because of :=, the inner money is a new variable that shadows the outer one, so the outer money never changes. Use = if you want to update the captured variable:
show := func() {
money = money + a + p // updates the captured variable
fmt.Println("Money =", money)
}
// 210, 320, 430, ...:= is one of the most common Go bugs, especially with err inside if blocks. go vet with the shadow analyzer can catch some cases.Closures in loops (and the Go 1.22 change)
Before Go 1.22, a for loop had one loop variable reused across iterations, so every closure saw its final value:
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() { fmt.Print(i, " ") })
}
for _, f := range funcs {
f()
}
// Go 1.22+: 0 1 2
// Go 1.21 and older: 3 3 3Since Go 1.22 each iteration gets its own i, so this prints 0 1 2. That fixed a whole class of goroutine bugs. If your go.mod declares an older Go version, you still get the old behaviour, which is why older code passes the value as an argument: go func(i int) { ... }(i).
Practical uses
Configuration and generators: return a function pre-loaded with settings.
func multiplier(factor int) func(int) int {
return func(n int) int { return n * factor }
}
double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5), triple(5)) // 10 15Middleware: wrap behaviour around another function. The web chapter builds HTTP middleware exactly like this.
func logged(name string, fn func()) func() {
return func() {
start := time.Now()
fn()
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}Key takeaways
:= shadowing inside closures.Last updated on