Explain closures in Go: what they capture, when captured variables escape to the heap, and how to build a generator or middleware with them.
Question 552MediumGo 1.22 to 1.25
A closure captures variables, not values, by reference. It sees later writes, and its own writes are visible outside. If the closure outlives the stack frame (it is returned, stored, or run in a goroutine), escape analysis moves the captured variables to the heap. Check with go build -gcflags=-m ("moved to heap: n"). A closure that is called immediately and never escapes may keep them on the stack.
func counter() func() int {
n := 0 // escapes: the returned closure references it
return func() int { n++; return n }
}
// Go 1.23 range-over-func generator
func Fib() iter.Seq[int] {
return func(yield func(int) bool) {
for a, b := 0, 1; ; a, b = b, a+b {
if !yield(a) {
return
}
}
}
}
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // "next" captured from the outer call
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
What does this print?
x := 1
f := func() { fmt.Println(x) }
x = 2
f() // 2: captured by reference
var fns []func()
for i := range 3 {
fns = append(fns, func() { fmt.Print(i) })
}
for _, fn := range fns {
fn()
} // Go 1.22+: 012 (each iteration has its own i). Before 1.22 / go.mod < 1.22: 333
Gotcha: the per-iteration change depends on the go line in go.mod, not the toolchain version. Capturing a large struct keeps all of it alive (a memory retention bug).
More on Go Idioms, Design Patterns & Language Design
- Q550How do you model enums in Go with iota? How do you add String() via go generate stringer, validate values, and handle the zero value?
- Q551How do variadic functions work? What is the aliasing trap when passing s... to a function that modifies or appends to its argument?
- Q553Why does Go have no inheritance, function overloading, default arguments, ternary operator or exceptions? How do idiomatic Go programs work around each?
- Q554Can you compare functions in Go? Why is a func only comparable to nil, and how does that affect using funcs as map keys?
- Q555Does Go do tail-call optimization? What happens with deep recursion, and what does "goroutine stack exceeds 1000000000-byte limit" mean?
- Q556How do you structure a large Go project (cmd/, internal/, pkg/ debate)? How should packages be named and split to avoid stutter and cycles?