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.
Quiz
1. What does this print? ```go a := counter() b := counter() fmt.Println(a(), a(), b()) ```
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) }() } ```
3. You need the value of `x` as it was when the closure was created, not its later values. What is the correct construction?
4. A closure increments a captured `total` from 100 goroutines, and `-race` reports a data race. What does this tell you?
Flashcards
Question
When a closure refers to a variable from its enclosing scope, does it capture the value or the variable?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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) }()`?
Click to reveal answer
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.