Function Values and Closures
Learn how Go treats functions as first-class values, how to write anonymous functions and function literals, and how closures capture and retain access to variables from their enclosing scope.
Functions in Go are not just named blocks of code you call. They are values that can be assigned to variables, passed into other functions, and returned from functions. This flexibility enables anonymous functions and closures — two features that make Go code more expressive and modular.
Functions as First-Class Values
In programming languages where functions are first-class citizens, you can treat a function like any other value: store it in a variable, hand it to another function as an argument, or return it from a function call. Go supports all of these.
A function type is determined by its parameter types and return types. Two functions with the same signature share the same type, even if they have completely different names (or no name at all).
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func multiply(a, b int) int {
return a * b
}
func main() {
var op func(int, int) int
op = add
fmt.Println(op(3, 4)) // 7
op = multiply
fmt.Println(op(3, 4)) // 12
}
The variable op holds a function that takes two int parameters and returns an int. add and multiply match that signature, so they can be assigned to op. The variable can then be called like a regular function.
This is not a feature you use for its own sake. It powers patterns where behaviour needs to vary at runtime — sorting with a custom comparator, filtering a collection, or injecting a callback into a library.
Function Types:
The full function signature — parameter types, order, and return types — defines the type. A function func(int) float64 is a different type from func(float64) int, even though they both take one number and return one number. They are not interchangeable.
Passing a function as an argument is straightforward. Here, apply takes a slice and an operation, then returns a new slice with the operation applied to every element.
func apply(nums []int, op func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = op(n)
}
return result
}
func double(x int) int {
return x * 2
}
func main() {
numbers := []int{1, 2, 3, 4}
doubled := apply(numbers, double)
fmt.Println(doubled) // [2 4 6 8]
}
Returning a function from another function opens the door to factories and configurable behaviour. That is where closures become essential.
Functions as Building Blocks:
When you see a function type as a parameter or return value, think of it as a pluggable piece of logic. The surrounding code defines the shape of the plug; you supply a function that fits.
Anonymous Functions and Function Literals
A function literal is a function defined without a name. You can write it inline and call it immediately, or assign it to a variable (which itself has a name, but the function body does not).
package main
import "fmt"
func main() {
// Define and invoke immediately
func(msg string) {
fmt.Println(msg)
}("Hello from an anonymous function")
// Assign to a variable for later use
square := func(x int) int {
return x * x
}
fmt.Println(square(5)) // 25
}
The first example declares a function that takes a string and prints it, then calls it straight away with "Hello from an anonymous function". The function body is executed once and never accessed again. The second example binds a function literal to square so it can be called multiple times.
Anonymous functions are common in situations where you need a one-off piece of logic and a full named function would add clutter. Sorting a slice with sort.Slice is a typical case.
people := []struct{ Name string; Age int }{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
The comparator function is used only once, right there. Writing a separate named function would break the reading flow.
A critical subtlety arises when anonymous functions capture variables from their enclosing scope. That is the essence of closures, explored in the next section. But even before formally discussing closures, one mistake shows up immediately when anonymous functions appear inside loops.
Loop Variable Capture:
If you define an anonymous function inside a for loop and the function references the loop variable, every invocation will see the same variable, not a snapshot of its value at that iteration. This is a common source of bugs, especially when launching goroutines.
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // all three goroutines print the final value of i (often 3)
}()
}
Fix it by passing the variable as an argument or creating a local copy inside the loop body before the anonymous function:
for i := 0; i < 3; i++ {
i := i // shadow variable — new `i` for this iteration
go func() {
fmt.Println(i)
}()
}
// Output: 0, 1, 2 (order may vary due to goroutines)
Closures
A closure is a function value that references variables from outside its body. The function “closes over” those variables, keeping them alive even after the enclosing function has returned. In Go, function literals are closures — they always capture the surrounding variables by reference.
The classic example is an adder that returns a function. Each returned function holds its own running total.
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 5; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}
Running this prints:
0 0
1 -2
3 -6
6 -12
10 -20
pos and neg each got their own sum from separate calls to adder. Each closure is bound to a distinct variable. No global state, no struct — just a function that remembers what it needs.
How Closures Work Internally
When the Go compiler sees a function literal that references a variable from an outer function, it cannot simply place that variable on the stack and forget it. The stack frame of the outer function would be destroyed when the function returns, but the closure still needs access to that variable later.
The compiler performs escape analysis. It detects that the variable’s lifetime extends beyond the outer function and moves it to the heap. The closure then holds a reference to that heap allocation. When all closures referencing the variable become unreachable, the garbage collector reclaims the memory.
A mental model: a closure is a function pointer plus a pointer to the environment (the captured variables). Every time you call the outer function, a fresh environment is created, so each closure gets its own independent copy of the variables it captures.
Practical Uses of Closures
Closures give functions a private memory. This is useful for:
- Stateful callbacks: A counter or token generator that increments on each invocation without exposing the counter variable.
- Configuration factories: A function that produces specialised functions based on parameters supplied once.
- Encapsulation without structs: When you need a single piece of encapsulated state and a struct feels too heavy, a closure can be simpler.
// intSeq returns a function that generates sequential integers,
// starting from 0 and incrementing by 1 each time it is called.
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
nextInt := intSeq()
fmt.Println(nextInt()) // 1
fmt.Println(nextInt()) // 2
fmt.Println(nextInt()) // 3
newInts := intSeq()
fmt.Println(newInts()) // 1 — fresh counter
Memory Retention:
A closure holds a reference to every variable it captures. If the captured variable is a large slice or a struct, that memory cannot be freed until the closure itself is garbage collected. In long‑running programs, be mindful of closures that outlive the need for the data they reference. Consider copying only what you need or designing the closure to release the reference when possible.
Independent State:
The intSeq example shows that each call to the outer function creates a new, isolated closure. There is no accidental sharing between nextInt and newInts. This makes closures a safe way to bundle data with behaviour.
Common Mistake: Capturing the Loop Variable
This was flagged earlier with anonymous functions, but it is worth repeating because it arises from the closure mechanism itself. When you capture a loop variable inside a closure defined within the loop body, all closures share the same variable. The variable’s value changes as the loop progresses, so by the time the closures execute (especially with goroutines), they all see the final value.
Always pass the loop variable as a parameter to the closure, or use the i := i shadowing trick to create a per‑iteration copy. The compiler allocates a new variable for each iteration with that idiom.
Summary
First-class functions are the foundation. Anonymous functions let you define behaviour inline without ceremony. Closures arise naturally when those anonymous functions capture variables from their surroundings — turning a function into a self‑contained unit of behaviour plus state.