Skip to content
Theory · Functions as values · lesson 3 of 12

Closures

In the previous lesson a function was a recipe card. Now let's clip a note to the card—a variable the recipe works with.

A function literal can be written inside main and stored in a variable. Such a literal sees the variables of main declared above it and can change them.

A literal sees variables

The literal add adds numbers to the variable total, which is declared in main:

GO
package main

import "fmt"

func main() {
    total := 0
    add := func(x int) { total += x }
    add(5)
    add(3)
    fmt.Println(total) // 8
}

Let's walk through the example line by line:

  1. total := 0 declares a variable in main.
  2. add := func(x int) { total += x } stores the literal in the variable add of type func(int). The literal is written inside main, so it sees the variables of main the same way the lines of main itself do.
  3. add(5) is called like an ordinary function and adds 5 to total in main.
  4. add(3) adds another 3, and total becomes 8.
  5. fmt.Println(total) prints 8.

The variable, not a copy

A function parameter is a copy of the argument, but a literal that uses total gets no copy. It works with the variable total itself, so the change lands in the same variable that main prints later.

A function that uses variables from the surrounding code is called a closure. The word has nothing to do with the "short-circuit" behavior of && and ||.

Here the note picture needs a caveat. A paper note is a copy: if you later fix the original, the note stays as it was. A closure holds not a copy but the variable itself, so the link works both ways. If you change the variable after the literal is created, a call sees the new value:

GO
x := 1
show := func() { fmt.Println(x) }
x = 2
show() // 2

Line by line:

  1. x := 1 declares a variable holding 1.
  2. show := func() { fmt.Println(x) } creates the literal while x holds 1.
  3. x = 2 changes the variable after the literal was created.
  4. show() reads x at the moment of the call and prints 2.

Several statements in a literal

A literal's body can hold several statements, and on one line they are separated by semicolons: func() int { n++; return n }. Such a literal runs them in order:

  1. n++ increments n.
  2. return n returns the already incremented value.

A closure from a function

A function can return a closure. The variable the closure uses keeps living after the function returns, as long as some variable holds the returned function, such as a below:

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

func main() {
    a, b := counter(), counter()
    fmt.Println(a(), a(), b()) // 1 2 1
}

Let's see what happens here:

  1. func counter() func() int declares a function that returns a function of type func() int.
  2. n := 0 creates a new variable n on every call to counter.
  3. return func() int { … } returns a new literal that increments this n and returns it.
  4. a, b := counter(), counter() calls counter twice. So a and b have separate counters—two cards, each with its own note.
  5. fmt.Println(a(), a(), b()) runs the calls in its argument list from left to right. The two calls to a give 1 and 2, and the first call to b gives 1 again.

Press "Next step" and watch the "captured" row: a and b each have their own n, and it lives on after counter returns.

LiveTwo countersstep 1 of 10 · frames: 1

main calls counter twice: the first result goes to a, the second to b.

The captured row shows the variables the closures hold

From outside, n cannot be changed: only the literal has access to it.

Closures in a loop

Interviewers like to ask this: what do closures created in the loop for i := 0; i < 3; i++ return if each returns i? The answer depends on the Go version:

  • since Go 1.22, each iteration has its own variable i, so the closures return 0, 1 and 2;
  • in older versions all iterations shared one variable, and after the loop every closure saw 3.

Common mistake

The literal tries to call itself by the name of the variable it is being assigned to:

GO
fact := func(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1) // ✗
}
TEXT
undefined: fact

The name fact appears only after the whole := statement, and inside the literal it does not exist yet. The fix takes two steps:

  1. Declare the variable first: var fact func(int) int.
  2. Assign the literal separately: fact = func(n int) int { … }.

The literal then uses the already declared variable fact.

Key points

  • A closure is a function that uses variables from the surrounding code.
  • A closure works with the variable itself, not a copy, so changes are visible both ways.
  • Each call to counter() creates a new variable n, so every counter keeps its own state.
  • Since Go 1.22, each iteration of a for loop has its own variable, and closures from the loop see different values.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way