16. Closures

📖 Reading · 9 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

An anonymous function can use variables from the function that surrounds it. When it does, it closes over them — it keeps them alive, and keeps seeing their current values. That's a closure, and it's how you build functions with memory.

A function that remembers

package main

import "fmt"

func main() {
    count := 0

    increment := func() int {
        count++
        return count
    }

    fmt.Println(increment())
    fmt.Println(increment())
    fmt.Println(increment())
    fmt.Println("count is now", count)
}

increment doesn't take count as a parameter and doesn't declare its own. It reaches out to main's count and modifies it. Every call sees the change the last one made.

The key point: the closure captures the variable, not a copy of its value. There is exactly one count here, and both main and increment are looking at it.

Returning a closure

This is where it becomes a tool. A function can build a closure and hand it back — and the captured variable survives even though the outer function has returned:

package main

import "fmt"

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

func main() {
    a := counter()
    b := counter()

    fmt.Println(a(), a(), a())
    fmt.Println(b())
}

a prints 1, 2, 3 and b prints 1. Each call to counter() creates a fresh n, and the returned closure keeps its own alive. n isn't on the stack any more — Go noticed it outlives counter and moved it to the heap for you. No manual memory management, no dangling pointer.

Configuring behaviour

The most common practical use: a function that builds a specialised function.

package main

import "fmt"

func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

func main() {
    double := multiplier(2)
    triple := multiplier(3)

    fmt.Println(double(10), triple(10))
    fmt.Println(double(triple(2)))
}

double and triple are the same code with different captured state. You just wrote a tiny factory.

The same trick makes middleware, retry wrappers and rate limiters read nicely — each is a function that wraps and returns another function.

Closures with defer

Remember from the control-flow module that a deferred call's arguments are evaluated immediately. Defer a closure instead and it reads the variable at the moment it runs, not at the moment you deferred it:

package main

import "fmt"

func main() {
    total := 0

    defer func() {
        fmt.Println("final total:", total)
    }()

    total += 10
    total += 5
    fmt.Println("working... total is", total)
}

The deferred closure prints 15, because it reads total when it runs at the end of main. This is the standard way to log a summary, record a duration, or inspect a result on the way out of a function.

The classic trap: capturing a loop variable

Here is the bug every Go programmer writes once. Suppose you build a slice of closures inside a loop:

funcs := []func(){}
for _, name := range []string{"a", "b", "c"} {
    funcs = append(funcs, func() {
        fmt.Println(name) // which name?
    })
}
for _, f := range funcs {
    f()
}

In Go 1.21 and earlier, name was a single variable reused by every iteration, so all three closures shared it and all three printed c — the last value. It caused so many bugs that the language changed: since Go 1.22, each iteration gets its own name, and this prints a b c.

The old fix, which you'll still see everywhere in existing code, is to make the copy explicit — either shadow the variable or pass it as a parameter:

package main

import "fmt"

func main() {
    funcs := []func(){}
    for _, name := range []string{"a", "b", "c"} {
        name := name // explicit per-iteration copy
        funcs = append(funcs, func() {
            fmt.Println(name)
        })
    }
    for _, f := range funcs {
        f()
    }
}

name := name looks absurd until you know why it's there. On modern Go it's redundant; in older code it's load-bearing. Either way, the underlying rule is the one to remember: a closure captures the variable, not the value. Whenever a closure outlives the loop that made it — and goroutines are the big case — ask yourself which variable it's actually holding.

Your turn

Write accumulator, which returns a function that adds its argument to a running total and returns the new total. The program should print exactly:

10
30
33
package main

import "fmt"

// write accumulator here — it returns a func(int) int

func main() {
    add := accumulator()
    fmt.Println(add(10))
    fmt.Println(add(20))
    fmt.Println(add(3))
}
package main

import "fmt"

func accumulator() func(int) int {
    total := 0
    return func(n int) int {
        total += n
        return total
    }
}

func main() {
    add := accumulator()
    fmt.Println(add(10))
    fmt.Println(add(20))
    fmt.Println(add(3))
}

That completes functions. Next module: the containers you'll put your data in — slices, arrays and maps.