ZetCode

Go closure

last modified October 30, 2026

In this article we show how closures work in Go.

Functions are first-class values in Go: they can be assigned to variables, stored in slices and maps, passed to other functions and returned from them.

A closure is an anonymous function that references variables from the surrounding function. The closure keeps those variables alive after the outer function returns, and every call sees the current value of the captured variable. This makes closures useful for counters, function factories, callbacks, sorting comparators and memoization.

All examples in this article were compiled and run with Go 1.27.0.

Immediately invoked function

An anonymous function literal can be called right after its definition. The call is written after the closing brace, and the result can be assigned like the result of a named function.

main.go
package main

import "fmt"

func main() {

    sum := func(a, b, c int) int {
        return a + b + c
    }(3, 5, 7)

    fmt.Println("3+5+7 =", sum)
}

The parameters 3, 5 and 7 are passed to the function literal, which returns their sum. This pattern is useful for short calculations that do not deserve a name.

$ go run main.go
3+5+7 = 15

A counter closure

The intSeq function declares a local variable and returns a function that increments it. The variable i would normally disappear when intSeq returns, but the returned closure keeps it alive.

main.go
package main

import "fmt"

func intSeq() func() int {
    i := 0

    return func() int {
        i++
        return i
    }
}

func main() {

    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())

    nextInt2 := intSeq()
    fmt.Println(nextInt2())
}

Each call of nextInt increments the same captured variable, so the calls return 1, 2, 3 and 4. The second sequence starts from its own zero.

$ go run main.go
1
2
3
4
1

Independent instances

Every call of the factory creates a new environment for the captured variable. Two counters made by the same function therefore keep separate state.

main.go
package main

import "fmt"

func newCounter() func() int {
    count := 0

    return func() int {
        count++
        return count
    }
}

func main() {

    a := newCounter()
    b := newCounter()

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

The counters advance independently: a reaches 2 while b is still at 1. A third call of a continues from 3.

$ go run main.go
1 2
1
3 2

Captured variables are shared

A closure does not receive a copy of the variable. It refers to the variable itself, so changes made through the closure are visible to the surrounding function.

main.go
package main

import "fmt"

func main() {

    x := 0

    inc := func() {
        x++
    }

    inc()
    inc()
    inc()

    fmt.Println(x)
}

The three calls increment the same x, and the final print shows 3. The closure and the outer function share one variable, not three copies of its value.

$ go run main.go
3

Loop variables in closures

Since Go 1.22 the loop variable in a for statement is declared anew in every iteration. Closures created inside the loop capture the variable of their own iteration.

main.go
package main

import "fmt"

func main() {

    var funcs []func() int

    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() int {
            return i
        })
    }

    for i, f := range funcs {
        if i > 0 {
            fmt.Print(", ")
        }
        fmt.Print(f())
    }
    fmt.Println()
}

The functions return 0, 1 and 2. In Go 1.21 and earlier the three closures shared one loop variable and all of them would return 3; the workaround was i := i inside the loop body.

$ go run main.go
0, 1, 2

Function factory

A function can return a closure that remembers its arguments. The multiplier function builds functions that multiply by a fixed factor.

main.go
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(5), triple(5))
    fmt.Println(double(triple(2)))
}

double and triple are separate closures with separate captured factors. The last line nests the calls and returns 12.

$ go run main.go
10 15
12

Closures as arguments

Functions that take a function parameter are called higher-order functions. The closure below is the transformation applied to every element of a slice.

main.go
package main

import "fmt"

func transform(nums []int, f func(int) int) []int {
    out := make([]int, len(nums))

    for i, n := range nums {
        out[i] = f(n)
    }

    return out
}

func main() {

    nums := []int{1, 2, 3}

    squares := transform(nums, func(n int) int {
        return n * n
    })

    fmt.Println(squares)
}

The anonymous function squares its argument, and transform applies it to all three numbers. The same helper can be reused with any other operation.

$ go run main.go
[1 4 9]

Sorting with a closure

The slices.SortFunc function takes a comparison closure. It must return a negative number when the first argument sorts before the second, zero when they are equal, and a positive number otherwise.

main.go
package main

import (
    "cmp"
    "fmt"
    "slices"
)

type Person struct {
    Name string
    Age  int
}

func main() {

    people := []Person{
        {"Lucia", 32},
        {"Jan", 45},
        {"Peter", 28},
    }

    slices.SortFunc(people, func(a, b Person) int {
        return cmp.Compare(a.Age, b.Age)
    })

    fmt.Println(people)
}

The comparator uses cmp.Compare on the ages, so the people are sorted from the youngest to the oldest. Swapping the arguments would reverse the order.

$ go run main.go
[{Peter 28} {Lucia 32} {Jan 45}]

Memoization

A closure can wrap a function with a cache. The map is created once, captured by the returned function, and reused on every call.

main.go
package main

import "fmt"

func memoize(f func(int) int) func(int) int {
    cache := make(map[int]int)

    return func(n int) int {
        if v, ok := cache[n]; ok {
            return v
        }
        v := f(n)
        cache[n] = v
        return v
    }
}

func main() {

    calls := 0

    square := memoize(func(n int) int {
        calls++
        return n * n
    })

    fmt.Println(square(4), square(4), square(5))
    fmt.Println("calls:", calls)
}

The second call with 4 returns the cached 16 and does not invoke the wrapped function, so the counter shows only two real calls.

$ go run main.go
16 16 25
calls: 2

Deferred closure

A deferred closure runs when the surrounding function returns. Because it can capture the named result, it can change the value that the caller receives.

main.go
package main

import "fmt"

func run() (result int) {
    defer func() {
        result *= 2
    }()

    return 5
}

func main() {
    fmt.Println(run())
}

The return 5 assigns 5 to result, then the deferred closure doubles it before the function actually returns, so the caller gets 10.

$ go run main.go
10

Accumulator

A closure can keep a running total across calls. The initial value is a parameter of the factory, and every call updates the captured variable.

main.go
package main

import "fmt"

func accumulator(start int) func(int) int {
    total := start

    return func(n int) int {
        total += n
        return total
    }
}

func main() {

    acc := accumulator(10)

    fmt.Println(acc(5), acc(-3), acc(0))
}

The accumulator starts at 10, adds 5 and 3, and returns the current total each time: 15, 12 and 12.

$ go run main.go
15 12 12

Closures in goroutines

A closure started with go runs concurrently with the rest of the program. When several goroutines update the same captured variable, the update must be synchronized; atomic.Int64 is one way to do it.

main.go
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {

    var counter atomic.Int64
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)

        go func() {
            defer wg.Done()
            counter.Add(1)
        }()
    }

    wg.Wait()
    fmt.Println(counter.Load())
}

The WaitGroup waits for all 100 goroutines, and the atomic counter makes every increment safe. The final value is always 100, although the goroutines run in an unspecified order.

$ go run main.go
100

Source

Go function literals - specification

Go sync/atomic package - reference

In this article we have shown how closures work in Go: anonymous functions, captured variables and their lifetime, independent instances, the loop variable rules of Go 1.22, function factories, higher-order functions, comparators, memoization, deferred closures, accumulators, and closures in goroutines.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.