How to Use Anonymous Functions (Closures) in Go

Define anonymous functions in Go using the func keyword without a name to create closures that capture surrounding variables.

You need logic that lives with the data

You're processing a batch of sensor readings. Some readings need to be scaled by a factor that changes every hour. You could write a named function scaleReading(factor float64, value float64), but that clutters the namespace with a helper you'll never call from anywhere else. You need the logic to live right next to the data that drives it. Go lets you define functions inline, capture the surrounding variables, and pass them around like any other value.

Anonymous functions and closures

Anonymous functions are functions without names. You define them with the func keyword and assign them to variables or pass them directly as arguments. When an anonymous function references variables from its surrounding scope, it becomes a closure. The closure bundles the function code with references to those variables. The variables stay alive as long as the closure exists, even if the outer function has returned.

Think of a closure like a vending machine. The machine is the function. The snacks inside are the captured variables. You can hand the machine to someone else, and when they press the button, it still has the snacks you loaded. The original store can close, but the machine keeps working with its internal supply.

Minimal example: capturing a counter

Here's the simplest closure: a function that captures a counter and increments it.

package main

import "fmt"

func main() {
	// counter starts at zero inside main
	counter := 0

	// increment is an anonymous function that captures counter
	// Go creates a closure here: increment holds a reference to counter
	increment := func() {
		// modify the captured variable directly
		counter++
		fmt.Println(counter)
	}

	// invoke the closure twice to see the state persist
	increment()
	increment()
}
# output:
1
2

How the compiler handles closures

When the compiler sees a closure, it checks which variables are captured. If a variable is captured, the compiler allocates it on the heap instead of the stack. This ensures the variable survives beyond the scope where it was declared. The closure holds a pointer to that heap memory. Every time you call the closure, it reads and writes the same memory location.

Closures capture variables by reference, not by value. Changing the variable outside the closure changes what the closure sees. If you reassign the variable, the closure sees the new value. The closure doesn't make a copy of the variable; it keeps a live link to it.

Go treats functions as first-class values. You can assign a function to a variable, pass it to another function, or return it from a function. The type system tracks the signature strictly. If you try to assign a function with the wrong signature to a variable, the compiler rejects the program with a type mismatch error. Convention suggests naming the variable after the action, not the type. Use handler or compare, not fn.

Factory pattern: returning closures

Here's a factory function that returns closures configured with different multipliers. Each call to the factory creates a new closure with its own captured state.

package main

import "fmt"

// makeMultiplier returns a closure that multiplies by a fixed factor
func makeMultiplier(factor int) func(int) int {
	// factor is captured by the returned closure
	// each call to makeMultiplier creates a distinct factor variable
	return func(x int) int {
		return x * factor
	}
}

func main() {
	// double captures factor as 2 from the first call
	double := makeMultiplier(2)

	// triple captures factor as 3 from the second call
	triple := makeMultiplier(3)

	fmt.Println(double(5))
	fmt.Println(triple(5))
}
# output:
10
15

Each call to makeMultiplier creates a new factor variable on the heap. The returned closure captures that specific variable. double and triple are independent closures. Changing one doesn't affect the other. This pattern is useful for creating specialized functions without writing a separate named function for each configuration.

Realistic example: sorting with a dynamic key

Here's a closure used to sort a slice based on a dynamic key. The sort.Slice function expects a comparison function. We pass an anonymous function that captures targetAge to sort users by proximity to that age.

package main

import (
	"fmt"
	"sort"
)

type User struct {
	Name string
	Age  int
}

func main() {
	users := []User{
		{"Alice", 30},
		{"Bob", 25},
		{"Charlie", 35},
	}

	// targetAge defines the reference point for sorting
	targetAge := 28

	// sort.Slice takes a comparison function as its second argument
	// the anonymous function captures targetAge from the surrounding scope
	sort.Slice(users, func(i, j int) bool {
		// compute absolute difference for index i
		diffI := users[i].Age - targetAge
		if diffI < 0 {
			diffI = -diffI
		}

		// compute absolute difference for index j
		diffJ := users[j].Age - targetAge
		if diffJ < 0 {
			diffJ = -diffJ
		}

		// return true if i is closer to targetAge than j
		return diffI < diffJ
	})

	fmt.Println(users)
}
# output:
[{Bob 25} {Alice 30} {Charlie 35}]

The sort.Slice call invokes the closure multiple times during the sort. Each invocation accesses the same targetAge variable. The closure doesn't copy targetAge; it reads it directly. This makes the sort logic flexible without creating a new named function for every possible age. You can change targetAge before the sort, and the closure uses the updated value.

Pitfalls and compiler errors

Closures are powerful, but they have pitfalls. The most common issue involves loops. In Go versions before 1.22, the loop variable was reused for every iteration. If you defined a closure inside a loop, all closures captured the same variable. By the time the closures ran, the loop had finished, and every closure saw the final value. Go 1.22 changed the language so each iteration gets its own variable. This makes loop captures safe by default. If you maintain code for older versions, you must copy the loop variable before capturing it. The compiler rejects code with loop variable i captured by func literal if you try to capture a loop variable in a way that conflicts with version-specific rules.

Another pitfall is capturing mutable state in concurrent code. If multiple goroutines call the same closure and modify captured variables, you create a data race. The race detector will flag this at runtime. Use mutexes or channels to protect shared state, or avoid mutable captures in concurrent closures. Goroutine leaks can also happen if a closure captures a channel and waits on it indefinitely. Always ensure there is a cancellation path.

You can also run into type errors when passing closures. If you pass a closure to a function that expects a specific signature, the compiler checks the types. If the closure returns the wrong type or takes the wrong arguments, the compiler complains with cannot use func() as type func(int) in argument. Fix the signature to match the expected type.

Decision matrix

Use an anonymous function when the logic is short, used only once, and depends on variables defined in the current scope. Use a named function when the logic is reusable, complex enough to warrant documentation, or needs to be tested independently. Use a method when the function operates on a specific type and belongs conceptually to that type's behavior. Use a closure when you need to bundle state with behavior, such as configuring a handler with a database connection or creating a factory that returns specialized functions. Use a struct with methods when the state is large, has multiple behaviors, or needs to be passed around as a single unit with a clear interface.

Closures capture variables, not values. Trust the reference. Keep closures short. If it grows, extract it.

Where to go next