Skip to content
intermediate Phase 3 · Functions, Defers, and Error Handling

Anonymous Functions and Closures

Create function values inline and capture surrounding variables by reference.

40m
0 problems
Topic Progress 0%

A Counter With No Global Variable

You need an access counter for three routes: /, /pricing, and /docs. Each route must report how many times it was hit, and the counts must stay independent. The naive move is three package-level ints — until the counters are needed per-instance, per-test, per-tenant, and the globals multiply. What you actually want is a function that hands out a fresh counter each time, without a struct and without shared state.

Go lets you write a function literal anywhere an expression is allowed, and a function literal that refers to variables from its enclosing scope is a closure — it carries those variables with it:

func counter() func() int {
	n := 0                     // one variable per call to counter()
	return func() int {
		n++                      // reaches out to the enclosing n
		return n
	}
}

hits := counter()
fmt.Println(hits(), hits(), hits()) // 1 2 3

pricing := counter()                // a completely separate n
fmt.Println(pricing())              // 1

Two things are happening that deserve a precise mental model. First, n is not copied into the returned function — the closure holds a reference to the variable that counter created. Second, each invocation of counter() creates a new variable n, so the two closures never touch each other's state. The returned function is a value like any other: store it, pass it, return it (Function Declarations and Multiple Returns).

Capture is by reference, not by snapshot:

x := 10
f := func() int { return x }
x = 20
fmt.Println(f()) // 20 — f reads x's current value

That is usually what you want (the closure sees the latest state), and it is the source of nearly every closure bug you will write: the variable keeps changing after the closure is created, sometimes from another goroutine. The next chapter covers the most famous version of that bug — the loop variable that a whole generation of Go programmers watched print 3 3 3 instead of 0 1 2.

Three Goroutines, One Variable, Last Writer Wins

A worker pool launches jobs in a loop and, for years, this looked correct:

var fns []func()
for i := 0; i < 3; i++ {
	fns = append(fns, func() { fmt.Println(i) })
}
for _, fn := range fns {
	fn()
}

Before Go 1.22, that printed 3 3 3. The loop declared i once, and every closure captured that one variable — by the time the functions ran, the loop had already driven it to 3. The goroutine version of the same bug is worse: launch ten goroutines in a loop that each read job, and they all read whatever job last held.

Go 1.22 changed the language: for now creates a fresh loop variable per iteration, for both three-clause loops and range. The same snippet prints 0 1 2, and capturing the range variable inside a goroutine is safe by default. The change is gated on the go directive in your go.mod — a module declaring go 1.21 or older keeps the shared-variable behavior even when compiled with a new toolchain.

The workaround that worked before and still works everywhere is to copy the variable inside the iteration:

for _, job := range jobs {
	job := job // fresh variable for this iteration
	go func() { process(job) }()
}

Worth remembering even on 1.22+, because it makes the intent explicit and compiles unchanged on older modules. Check your go.mod before assuming which semantics you have.

The underlying rule has not changed at all: a closure captures variables, not values, and it keeps reading the variable itself. Per-iteration variables simply give each iteration a different variable to capture. The same rule explains why this still behaves surprisingly:

x := 1
f := func() int { return x }
x = 2
fmt.Println(f()) // 2, not 1

If you need the value as it was when the closure was created, pass it as a parameter instead — func(n int) func() int { return func() int { return n } } freezes the input at creation time. Choosing between "remember this value" and "watch this variable" is the entire closure design decision.

Behavior on the Right-Hand Side of an Assignment

Once functions are values, code that used to require a named helper becomes an inline expression. Sorting a slice by a custom rule is the everyday example:

sort.Slice(users, func(i, j int) bool {
	return users[i].Name < users[j].Name
})

The comparator needs no name because it is used exactly once, at this call site, and it closes over users — the surrounding data is right there. The same shape covers HTTP handlers (func(w http.ResponseWriter, r *http.Request) wrapped in a middleware chain), filtering a slice with a predicate, and time.AfterFunc(d, func() { ... }).

Returning a closure builds a factory: a function configured once, reused many times.

func retry(delays ...time.Duration) func(attempt func() error) error {
	return func(attempt func() error) error {
		var err error
		for i, d := range delays {
			if err = attempt(); err == nil {
				return nil
			}
			if i < len(delays)-1 {
				time.Sleep(d)
			}
		}
		return fmt.Errorf("gave up after %d attempts: %w", len(delays), err)
	}
}

withBackoff := retry(100*time.Millisecond, 500*time.Millisecond)
_ = withBackoff(func() error { return ping() })

retry captures delays; each call to withBackoff reuses that slice. Notice what this design does for testing: the schedule is data, so a test can build retry(time.Microsecond) without touching production code.

Closures also carry deferred evaluation — the body does not run until someone calls the function. That distinction matters as soon as defer enters the picture: defer fmt.Println(i) evaluates i immediately and prints the snapshot later, while defer func() { fmt.Println(i) }() evaluates nothing until the deferred call runs and reads the variable's final state (defer and the Deferred Call Stack). Both forms are correct; they answer different questions, and knowing which one you wrote is the difference between debugging a print order and debugging nothing.

The flip side: an anonymous function is the right tool only while it stays obvious. The moment its body grows past a handful of lines, needs its own tests, or is referenced twice, give it a name — a named function is greppable, doc-commentable, and stack-traceable, while a long literal buried inside another function makes the outer one hard to read. The working rule from the standard library's own source: literals for the short, local, use-once behavior; named functions for anything with a name worth saying out loud.

State That Outlives the Loop — Sometimes Too Much

A closure's captured state lives as long as the closure does. That property powers the counter factory and also produces two failure modes worth recognizing on sight.

Captured state shared across goroutines is ordinary shared state. This compiles and races:

var total int
var wg sync.WaitGroup
for _, n := range nums {
	wg.Add(1)
	go func() {          // captures total and n by reference
		total += n
		wg.Done()
	}()
}
wg.Wait() // data race under -race, lost updates regardless

A closure changes nothing about the memory model: two goroutines writing the same captured variable is the same race as two goroutines writing a package-level variable (Race Detector). The fix is the usual one — a mutex, atomics, or channels to carry results back — plus passing n explicitly so the intent is visible.

A closure keeps everything it references alive. A handler that captures a 50 MB cache object to read one field pins that object for the handler's whole lifetime, even after the cache entry is evicted. Long-lived closures should capture small, stable values — ids, config, a narrow interface — not the large structure they were typed next to. If a closure only needs one field of a big struct, capture the field.

The judgment call is about who owns the state. A closure is the right home for state with one owner and one lifecycle: a request-scoped accumulator, a per-test counter, a retry schedule. State that several packages read, outlives every closure that references it, or must be visible in a struct's API belongs in a named type with methods (Methods with Receivers) — you can see it, document it, and reset it deliberately.

For tests, closures are the cheap version of a fake: capture a variable, assert on it after the call.

var sent []string
send := func(msg string) { sent = append(sent, msg) }
publish("boot", send)
if len(sent) != 1 {
	t.Fatalf("published %d messages, want 1", len(sent))
}

No mocking framework, no global — just captured state with an owner you can see.

Quiz

1. What does this print? ```go a := counter() b := counter() fmt.Println(a(), a(), b()) ```

Question 1 options

2. On a module with `go 1.21` in go.mod, this loop launches three goroutines that each print their captured variable. What is observed? ```go for i := 0; i < 3; i++ { go func() { fmt.Print(i) }() } ```

Question 2 options

3. You need the value of `x` as it was when the closure was created, not its later values. What is the correct construction?

Question 3 options

4. A closure increments a captured `total` from 100 goroutines, and `-race` reports a data race. What does this tell you?

Question 4 options

Flashcards

Question

When a closure refers to a variable from its enclosing scope, does it capture the value or the variable?

Answer

The variable. The closure keeps reading that variable's current state, so later assignments are visible through the closure. To freeze a value at creation time, pass it as a parameter (or copy it to a new local) and capture that instead.

Question

What was the classic loop-variable capture bug, and what changed in Go 1.22?

Answer

Before 1.22, a for loop declared one variable shared by all iterations, so closures and goroutines in the loop all read the final value (printing 3 3 3 instead of 0 1 2). Go 1.22 gives each iteration a fresh variable, gated by the go directive in go.mod — modules declaring go 1.21 or older keep the old behavior.

Question

What is a closure factory and when is it better than a struct with methods?

Answer

A function that returns a function configured by its arguments — counter(), retry(delays...). It fits state with one owner and one lifecycle (a test counter, a per-request accumulator). Prefer a struct when state must be inspected, reset, shared across methods, or exposed in an API.

Question

How can a long-lived closure accidentally retain memory?

Answer

It pins every variable it references for as long as it exists, even if it only touches one field. A request handler that captures a large cache struct keeps that struct alive after eviction — capture the narrow field or id you actually need instead of the whole object.

Question

Why is `defer fmt.Println(i)` different from `defer func() { fmt.Println(i) }()`?

Answer

The first evaluates i when the defer statement runs and prints that snapshot at function exit; the second evaluates nothing until exit, then reads i's current (possibly final) value. The direct form defers a call with pre-evaluated arguments — the closure form defers the computation itself.

Revision Notes

Key Takeaways

  • 1. An anonymous function can appear anywhere an expression can; a closure additionally captures the enclosing scope's variables
  • 2. Captures are by reference: the closure watches the variable, so pass a parameter in when you need a frozen value
  • 3. Pre-1.22 loops shared one variable across iterations; Go 1.22 gives per-iteration variables, gated by the go.mod go directive
  • 4. Each call to a closure factory creates independent state — two counters never share n
  • 5. Captured state is shared state: same race rules, and long-lived closures pin everything they reference

Interview Tips

  • • "What is a closure in Go?" — a function value plus the environment it captured; immediately add that capture is by reference, which is where the interesting bugs live
  • • "This loop launches goroutines that print wrong values — why?" — name the shared loop variable, then give both fixes: `i := i` for any module, or the go 1.22 per-iteration change
  • • "Closure or struct with methods?" — one owner and one lifecycle versus state that needs inspection, reset, or sharing; show you know when the cheap tool stops being cheap
  • • "How would you test code that sends messages?" — inject a closure that appends to a local slice and assert on it after; no framework, no globals

Cheat Sheet

Anonymous Functions and Closures

Capture rules:

Code Reads Use when
f := func() int { return x } x's current value, always you want live state
f := func(n int) func() int { return func() int { return n } } n's creation-time value you want a snapshot
for i := range xs { go func(){ use(i) }() } fresh i per iteration (go 1.22+) modern modules
same, on go 1.21 go.mod one shared i — bug add i := i

Decision rules: state with one owner and lifecycle -> closure; state needing inspection/reset/sharing -> struct with methods; value needed as-of-creation -> pass as parameter; closure capturing large struct -> capture the one field you need.

Code template:

func counter(start int) func() int {
	n := start
	return func() int {
		n++
		return n
	}
}

Watch for: captured variables written from multiple goroutines are an ordinary data race (guard with mutex/atomics/channels); check the go directive before reasoning about loop capture.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading