Functions as Values and Closures

In Go, functions are first-class values: you can assign a function to a variable, pass it as an argument to another function, return it from a function, and store it in a struct field or a slice, exactly the way you would with an int or a string. A closure is a function literal that references variables declared outside its own body — it “closes over” those variables, capturing them by reference so it can read and modify them even after the function that declared them has returned. Together, functions-as-values and closures are what let Go express callbacks, configuration options, middleware, and stateful generators without needing a class system.

Overview / How It Works

A function’s type in Go is described by its parameter types and return types, written as func(paramTypes) returnType. That signature is a type, the same way int or []string is a type. This means you can declare a variable whose type is a function signature: var op func(int, int) int declares a variable named op that can hold any function taking two ints and returning an int. You can assign a named function like add to it, call it through the variable, reassign it to a different function later, or pass it around as an argument — it behaves exactly like any other value.

A function literal (also called an anonymous function) is a function value written inline, without a name: func(n int) bool { return n > 0 }. When a function literal refers to a variable declared in the enclosing function — rather than one of its own parameters or locals — it becomes a closure over that variable. The closure does not copy the variable’s value at creation time; it keeps a live reference to the exact same storage location. If the closure (or the enclosing code) later changes the variable, every closure that captured it sees the new value.

This has a real consequence for memory layout. Normally, a local variable lives on the goroutine’s stack and disappears when its function returns. But if the Go compiler’s escape analysis determines that a variable is referenced by a closure that outlives the function — for example, because the closure is returned to the caller — the compiler allocates that variable on the heap instead of the stack, and the returned closure carries a pointer to it. This is why calling a “factory” function twice produces two completely independent closures, each pointing at its own heap-allocated variable, while two closures created within a single call to that factory (and returned together) can share the same captured variable and observe each other’s updates.

The zero value of any function type is nil. A nil function value can be compared to nil and passed around safely, but calling it panics at runtime with a nil pointer dereference — there is no function body to jump to. This matters for optional callback parameters: always check for nil before invoking a function value that might not have been set.

Syntax

// a variable whose type is a function signature
var op func(int, int) int

// a function literal (anonymous function)
func(n int) bool {
    return n > 0
}

// a function that accepts a function value as a parameter
func apply(n int, f func(int) int) int {
    return f(n)
}

// a function that returns a function value
func makeAdder(base int) func(int) int {
    return func(n int) int {
        return base + n
    }
}
Piece Meaning
func(int, int) int a function type: takes two ints, returns an int
func(n int) bool { ... } a function literal (value) with no name, usable immediately or assigned to a variable
parameter of type func(int) int lets a caller inject custom behavior (a callback)
return type func(int) int lets a function hand back a specialized function, often a closure

Examples

Example 1: assigning a named function to a variable.

package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	var op func(int, int) int
	op = add
	result := op(3, 4)
	fmt.Println(result)
}

Output:

7

Here op is declared with a function type, not a specific function. Assigning add to it just stores that function’s address and signature in op. Calling op(3, 4) calls whatever function op currently holds — in this case, add.

Example 2: a closure that carries private state.

package main

import "fmt"

func makeCounter() func() int {
	count := 0
	return func() int {
		count++
		return count
	}
}

func main() {
	counter := makeCounter()
	fmt.Println(counter())
	fmt.Println(counter())
	fmt.Println(counter())

	counter2 := makeCounter()
	fmt.Println(counter2())
}

Output:

1
2
3
1

makeCounter declares a local variable count and returns a function literal that references it. That returned function is a closure: it keeps count alive on the heap and increments the same variable on every call. counter2 comes from a fresh call to makeCounter, so it gets its own independent count, starting again at 1 — the two counters do not share state.

Example 3: passing a closure as a callback.

package main

import "fmt"

func filter(nums []int, keep func(int) bool) []int {
	var result []int
	for _, n := range nums {
		if keep(n) {
			result = append(result, n)
		}
	}
	return result
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	threshold := 5
	greaterThanThreshold := func(n int) bool {
		return n > threshold
	}
	result := filter(nums, greaterThanThreshold)
	fmt.Println(result)
}

Output:

[6 7 8 9 10]

filter takes a slice and a keep function and has no idea what condition it is testing — it just calls whatever function it was given. The closure greaterThanThreshold captures threshold from main‘s scope, so filter effectively runs with a condition that was configured by its caller. This is the core pattern behind callbacks, predicates, and much of the standard library’s sort and strings APIs.

How It Works Step by Step

Walking through Example 2 in detail:

  • Calling makeCounter() starts a new invocation of that function; a local variable count is created and set to 0.
  • makeCounter builds a function literal that reads and writes count, and returns that literal as its result.
  • Because the returned closure references count and must keep working after makeCounter returns, the compiler’s escape analysis places count on the heap rather than the stack, and the closure value stores a pointer to it.
  • makeCounter returns; its stack frame is popped, but count survives on the heap because the closure still points to it.
  • Each call to counter() runs the closure body, which increments the heap-allocated count through the stored pointer and returns the new value — this is why the printed sequence is 1, 2, 3.
  • Calling makeCounter() a second time repeats the whole process with a brand-new count variable, so counter2 starts fresh at 1, completely independent of counter.

Common Mistakes

Mistake 1: capturing a loop variable by reference instead of by value. A closure created inside a loop captures the loop variable itself, not a snapshot of its value at that iteration. Historically (before Go 1.22) this was a frequent source of bugs, because every closure ended up sharing the same variable:

// WRONG (classic pre-1.22 bug; still worth guarding against)
package main

import "fmt"

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

On Go versions before 1.22, every closure in funcs captured the same i, so by the time the loop finished and the closures ran, all of them printed 3 (the final value of i). Go 1.22 changed the language so that each loop iteration gets its own copy of i, which makes this particular snippet print 0, 1, 2 instead — but you should still write the defensive, version-independent fix, either by shadowing the variable inside the loop body or by passing it as an explicit parameter:

package main

import "fmt"

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

Output:

0
1
2

The line i := i declares a brand-new variable named i, scoped to that single iteration, and each closure captures its own copy. The same pattern applies to goroutines launched inside a loop: prefer go func(i int) { ... }(i) so the value is passed explicitly rather than relying on shared closure state.

Mistake 2: calling a function value that might be nil. An uninitialized variable of function type is nil, and calling it panics:

// WRONG: panics with a nil pointer dereference
package main

func main() {
	var handler func()
	handler()
}

This compiles cleanly because handler is a perfectly valid, if empty, function value — the panic only happens at runtime, when there is no function body behind the pointer to jump to. Guard any optional callback with a nil check before calling it:

package main

import "fmt"

func safeCall(handler func()) {
	if handler != nil {
		handler()
	} else {
		fmt.Println("no handler set")
	}
}

func main() {
	var handler func()
	safeCall(handler)

	handler = func() {
		fmt.Println("handler called")
	}
	safeCall(handler)
}

Output:

no handler set
handler called

Best Practices

  • Give a repeated function signature a named type (type Predicate func(int) bool) so parameter lists and doc comments read cleanly instead of repeating the raw signature everywhere.
  • Keep closures small and focused; if a closure grows past a few lines or needs to be tested in isolation, promote it to a named function.
  • Never capture a loop variable directly in a closure or goroutine without either shadowing it (i := i) or passing it as an explicit parameter — do this defensively even on Go 1.22+, since the code may be copied into an older module or a different loop construct.
  • Document what state a returned closure captures, especially if multiple closures from the same factory call share mutable state — callers need to know that calling one affects the others.
  • If closures capturing shared state are called from multiple goroutines, protect that state with a sync.Mutex or communicate through a channel instead of relying on unsynchronized reads and writes.
  • Always check a function-typed value for nil before calling it when the value is optional (a callback that a caller might not supply).

Practice Exercises

Exercise 1: Write a function makeMultiplier(factor int) func(int) int that returns a closure multiplying its argument by factor. Create two multipliers, one for 2 and one for 10, and print the result of applying each to the number 7. Expected output: 14 then 70.

Exercise 2: Write a function mapInts(nums []int, f func(int) int) []int that applies f to every element of nums and returns a new slice of the results. Use it with a closure that squares each number in []int{1, 2, 3, 4}.

Exercise 3: Write a function makeAccumulator() func(int) int that returns a closure which, each time it is called with a number, adds that number to a running total and returns the new total. Call it three times with 10, 5, and 2, and verify the running totals are 10, 15, and 17.

Summary

  • Functions in Go are first-class values: they have types, can be stored in variables, passed as arguments, and returned from other functions.
  • A closure is a function literal that captures variables from its enclosing scope by reference, not by copying their value.
  • When a closure outlives the function that created it, escape analysis moves the captured variable to the heap so the closure can keep using it safely.
  • Two closures created by separate calls to the same factory function have independent captured state; closures created together within one call can share the same captured variables.
  • The zero value of a function type is nil; calling a nil function value panics, so guard optional callbacks.
  • Closures created inside loops must capture the loop variable defensively (via shadowing or an explicit parameter) to avoid the classic shared-variable bug.