Skip to Content
Go Realm v1 is released 🎉
TopicsClosure

Closures in Go

A closure is a function that captures and retains access to variables from its surrounding scope — even after that scope has returned.

🎒 Analogy: Think of a closure like a backpack. When a function leaves, it takes a backpack containing the variables it needs. Those variables live on as long as the backpack (closure) exists.


1. How Closures Work Internally

When a closure captures a variable, Go moves that variable from the stack to the heap — so it survives after the outer function returns. The closure and outer function share the same variable, not a copy.

func counter() func() int { count := 0 // moved to heap — shared with closure return func() int { count++ // modifies the shared variable return count } } c1 := counter() fmt.Println(c1()) // 1 fmt.Println(c1()) // 2 fmt.Println(c1()) // 3 c2 := counter() // independent — own heap variable fmt.Println(c2()) // 1

Each call to counter() creates a new, independent count on the heap.


2. Closure Captures by Reference — Not by Value

x := 10 add := func(n int) int { return x + n // captures x by reference } x = 20 fmt.Println(add(5)) // 25 — sees the updated x, not 10!

The closure doesn’t snapshot x at creation time — it holds a reference to the original variable.


3. The Loop Closure Trap ⚠️

Pre Go 1.22

funcs := make([]func(), 3) for i := 0; i < 3; i++ { funcs[i] = func() { fmt.Println(i) // ❌ all print 3 — all share same i } } for _, f := range funcs { f() } // Output: 3 3 3

Fix 1 — shadow with local copy:

for i := 0; i < 3; i++ { i := i // new variable per iteration funcs[i] = func() { fmt.Println(i) } }

Fix 2 — pass as argument:

for i := 0; i < 3; i++ { funcs[i] = func(n int) func() { return func() { fmt.Println(n) } }(i) }

Go 1.22+ — fixed automatically

Each loop iteration gets its own variable — no fix needed with go 1.22+ in go.mod.


4. Production Patterns

Stateful middleware (HTTP)

func withLogger(next http.HandlerFunc) http.HandlerFunc { count := 0 // closure captures count return func(w http.ResponseWriter, r *http.Request) { count++ log.Printf("request #%d: %s %s", count, r.Method, r.URL.Path) next(w, r) } }

Memoization

func memoize(fn func(int) int) func(int) int { cache := make(map[int]int) // closure captures cache return func(n int) int { if v, ok := cache[n]; ok { return v } cache[n] = fn(n) return cache[n] } } slowSquare := func(n int) int { return n * n } fastSquare := memoize(slowSquare) fmt.Println(fastSquare(5)) // computed fmt.Println(fastSquare(5)) // from cache

Functional options pattern

type ServerConfig struct{ port int; timeout time.Duration } type Option func(*ServerConfig) func WithPort(p int) Option { return func(c *ServerConfig) { c.port = p } } func WithTimeout(d time.Duration) Option { return func(c *ServerConfig) { c.timeout = d } }

5. Goroutines + Closures ⚠️

// ❌ Race condition — all goroutines share same i (pre Go 1.22) for i := 0; i < 5; i++ { go func() { fmt.Println(i) }() } // ✅ Pass i as argument — captures a copy for i := 0; i < 5; i++ { go func(n int) { fmt.Println(n) }(i) }

Closures that run in goroutines and mutate shared captured variables are not thread-safe — use sync.Mutex or sync/atomic.

var mu sync.Mutex count := 0 inc := func() { mu.Lock() defer mu.Unlock() count++ }

6. Memory Leak Warning

A closure keeps all its captured variables alive on the heap as long as the closure itself is reachable.

// ❌ Large slice kept alive because closure holds a reference func leaky() func() int { big := make([]int, 1_000_000) return func() int { return big[0] } } // ✅ Capture only what you need func safe() func() int { big := make([]int, 1_000_000) first := big[0] // copy only the value return func() int { return first } }

7. Common Mistakes ⚠️

MistakeProblemFix
Capturing loop variable in closureall closures see final valueshadow: i := i or use Go 1.22+
Goroutine closure without passing argdata race on shared varpass as goroutine argument
Mutating captured var from goroutinesrace conditionuse sync.Mutex or atomic
Closure holding large datamemory leakcapture only the needed value
Assuming closure snapshots the valueit holds reference, not copybe explicit about what changes

8. Interview Cheat Sheet

Q: What is a closure?

A function that captures variables from its surrounding scope. The captured variable is shared by reference — not copied — and lives on the heap as long as the closure is reachable.

Q: Difference between anonymous function and closure?

All closures are anonymous functions, but not all anonymous functions are closures. A closure specifically captures at least one variable from an outer scope.

Q: Does a closure capture by value or by reference?

By reference. The closure and the outer function share the same variable. Changing the variable in one affects the other.

Q: What happens to captured variables when the outer function returns?

Go’s escape analysis moves them from the stack to the heap automatically. They remain alive as long as the closure is reachable.

Q: Are closures thread-safe?

No. If multiple goroutines read/write the same captured variable, you need synchronization (sync.Mutex or sync/atomic).

Q: What is the classic loop closure bug?

All closures in the loop capture the same loop variable and see its final value when called. Fix: shadow with i := i, pass as arg, or use Go 1.22+.