Go

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

All 16 Go Idioms, Design Patterns & Language Design questions