Anonymous Functions
An anonymous function in Go is a function literal — a function value defined without a name, written directly at the point where it is needed instead of being declared with the usual func name(...) form at package level. You can assign it to a variable, pass it straight into another function as an argument, store it in a struct field, or call it immediately, all without ever giving it an identifier. Anonymous functions are the mechanism behind closures, one-off goroutines, and inline callbacks, and understanding them well is essential for writing idiomatic, concurrent Go.
Overview / How it works
In Go, a function is a value, just like an int or a string, and it has a type — for example func(int) int. A named function declared at package level, such as func Add(a, b int) int { ... }, is really just a function literal that has been bound to an identifier. Because functions are values, you can write the literal itself — the func keyword, parameter list, return type, and body — without a name, anywhere an expression is allowed: assigned to a variable, stored in a slice or map, passed as an argument, returned from another function, or invoked immediately.
The single most important idea behind anonymous functions is the closure. When an anonymous function refers to a variable declared outside its own body, it does not copy that variable — it captures it by reference, forming a closure over it. The function literal and the captured variable stay bound together for as long as either is reachable. This is why a closure returned from a function can keep reading and modifying a local variable of that function long after the function itself has returned. Normally a local variable’s stack frame would be gone by then, but the Go compiler performs escape analysis: it notices the variable is still referenced by a closure that outlives the current call, and allocates that variable on the heap instead of the stack so it survives. You never write any of this yourself — the compiler decides automatically whether a variable can stay on the stack or must escape to the heap, based purely on whether anything outside the function might still need it.
Anonymous functions show up constantly in idiomatic Go: as the comparison passed to sort.Slice, as the callback wrapped by http.HandlerFunc, as goroutine bodies launched with go func() { ... }(), as deferred cleanup logic with defer func() { ... }(), and as small one-off helpers that do not deserve a package-level name because they are only used in a single place. Because a closure captures variables by reference, and because Go runs goroutines concurrently, anonymous functions are also the source of one of the most common bugs in Go code: accidentally sharing a loop variable across multiple goroutines. That gets its own section below.
Syntax
The general form of a function literal looks exactly like a named function declaration, minus the name:
func(parameters) returnType {
// function body
}
// Assigned to a variable:
add := func(a, b int) int {
return a + b
}
// Called immediately (an "immediately invoked function expression", or IIFE):
func(msg string) {
fmt.Println(msg)
}("hello")
| Part | Meaning |
|---|---|
func |
The keyword that starts every function literal, named or anonymous. |
parameters |
A comma-separated parameter list, using the same rules as a named function, e.g. (a, b int). |
returnType |
Zero, one, or multiple return types; omitted entirely if the function returns nothing. |
{ ... } |
The function body; may reference any variable from the enclosing scope, which is what forms a closure. |
trailing (...) |
Optional. Appending parentheses (with arguments, if any) right after the closing brace calls the function immediately instead of storing it for later. |
Examples
Example 1: assigning and immediately invoking
package main
import "fmt"
func main() {
square := func(n int) int {
return n * n
}
fmt.Println(square(5))
result := func(a, b int) int {
return a + b
}(3, 4)
fmt.Println(result)
}
Output:
25
7
square and the second literal are both anonymous functions — function values with no name of their own. square is stored in a variable and called later via square(5); the second literal is called immediately by appending (3, 4) right after its closing brace, so it never needs to be stored in a variable at all. Both forms produce a value that fmt.Println can print like any other.
Example 2: a closure that keeps 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 returns a function literal that closes over the local variable count. Because the closure captures count by reference, count survives after makeCounter returns, living on the heap instead of the now-gone stack frame of makeCounter. Every call to counter() increments and returns the same count that only that closure owns. Calling makeCounter() a second time creates a brand-new count and a brand-new closure around it, so counter2 starts back at 1, completely independent of counter.
Example 3: anonymous functions as goroutines
package main
import (
"fmt"
"sync"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
results := make([]int, len(numbers))
var wg sync.WaitGroup
for i, n := range numbers {
wg.Add(1)
go func(i, n int) {
defer wg.Done()
results[i] = n * n
}(i, n)
}
wg.Wait()
fmt.Println(results)
}
Output:
[1 4 9 16 25]
Each iteration launches a goroutine as an anonymous function, but instead of letting the closure reach into the loop’s i and n directly, they are passed in as parameters, func(i, n int), and supplied as arguments, (i, n), when the literal is invoked. That gives every goroutine its own private copy of both values, so there is no risk of one goroutine seeing a value another iteration already changed. sync.WaitGroup makes main wait until all five goroutines have called wg.Done() (via the deferred call) before reading results, which is required — without it, main could print results before any goroutine had run.
How it works step by step
Walking through the closure in Example 2:
maincallsmakeCounter().- Inside
makeCounter,countis declared and initialized to0. - A function literal is created that references
count. The compiler’s escape analysis sees that this literal will outlivemakeCounter‘s own call frame, so it allocatescounton the heap instead of the stack. makeCounterreturns the literal — now a function value — whichmainstores incounter.- Each call to
counter()runs the literal’s body:count++mutates the same heap-allocatedcount, then returns its current value. - Calling
makeCounter()again repeats the earlier steps with a brand-newcount, producing a second, fully independent closure.
The same reference-capture behavior explains the goroutine pattern in Example 3, in the other direction: if the closure had referred to the loop’s i and n directly instead of taking them as parameters, every goroutine would share the same underlying variables rather than each getting its own value. Passing them in as arguments forces a fresh copy to be made at the moment each goroutine is launched.
Common Mistakes
Mistake 1: capturing the loop variable instead of copying it
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
// On Go 1.21 and earlier, every closure shares the same variable i,
// so this often prints "3 3 3" instead of "0 1 2" -- the loop can
// finish incrementing i to 3 before any goroutine actually runs.
// It's also missing synchronization, so main could exit before any
// goroutine runs at all.
Before Go 1.22, the loop variable i was a single variable reused across every iteration, so a closure that referenced i directly captured that shared variable, not a snapshot of its value at that iteration. Go 1.22 changed loop variables to be per-iteration by default, which fixes this specific case — but relying on that is fragile: the same bug still applies to any variable declared outside a loop and mutated inside it, and code that assumes 1.22+ semantics silently breaks on an older toolchain. The defensive, version-independent fix is to pass the value in explicitly as a parameter, forcing a copy at the moment the closure is created:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
results := make([]int, 3)
for i := 0; i < 3; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
results[i] = i
}(i)
}
wg.Wait()
fmt.Println(results)
}
Output:
[0 1 2]
Mistake 2: forgetting to call the function literal
addOne := func(x int) int {
return x + 1
}
total := 10 + addOne
fmt.Println(total)
// compile error: invalid operation: 10 + addOne (mismatched types
// untyped int and func(int) int) -- addOne is a function value here,
// not the int it returns, because the trailing (5) call was omitted.
A function literal assigned to a variable is a function value, not the value it will eventually return. Forgetting the trailing call — addOne instead of addOne(5) — is a type error the compiler will always catch, but it is a common slip when refactoring a plain expression into a closure. The fix is simply to call it:
package main
import "fmt"
func main() {
addOne := func(x int) int {
return x + 1
}
total := 10 + addOne(5)
fmt.Println(total)
}
Output:
16
Best Practices
- Keep anonymous functions short and single-purpose; if the logic grows complex or gets reused in more than one place, give it a name and promote it to a regular function.
- When a goroutine or closure needs a loop variable, pass it in explicitly as a parameter rather than relying on Go 1.22+’s per-iteration semantics — it documents your intent and works on every supported Go version.
- Use closures to encapsulate private state (like
makeCounter‘scount) instead of reaching for a package-level mutable variable. - Always pair goroutines launched from a loop with a
sync.WaitGroupor a result/done channel; never assume goroutines finish before the surrounding function returns. - Be careful with
defer func() { ... }()inside a loop — deferred closures also capture by reference and all run at the end of the enclosing function, not at the end of each iteration, so their captured variables may have already changed. - Favor named parameters over relying on capture when a closure’s behavior should be obvious from its call site — explicit arguments are easier to reason about than implicit captures.
Practice Exercises
- Write an anonymous function assigned to a variable named
doublethat takes anintand returns twice its value. Call it with21and print the result. Expected output:42. - Write a function
makeAccumulatorthat returns a closure. Each time the closure is called with anint, it should add that value to a running total and return the new total. CallingmakeAccumulator()twice should produce two independent accumulators. Model it onmakeCounterfrom this lesson, but give the returned closure a parameter instead of taking none. - Launch five goroutines, each computing the cube of its own index (0 through 4), storing results in a slice of length 5 using a
sync.WaitGroup. Pass the index into the goroutine literal as a parameter rather than capturing the loop variable directly. Expected output:[0 1 8 27 64].
Summary
- An anonymous function (function literal) is a function value written without a name, usable anywhere an expression is allowed.
- A literal can be assigned to a variable, passed as an argument, returned from a function, or invoked immediately by appending
(...)right after its body — an IIFE. - A closure is an anonymous function that captures variables from its enclosing scope by reference; the Go compiler uses escape analysis to move captured variables to the heap when needed so they outlive the function that declared them.
- Each call to the outer function that returns a closure creates a fresh, independent set of captured variables.
- Before Go 1.22, loop variables were shared across iterations, causing a classic goroutine-capture bug; passing loop values in as explicit parameters fixes it portably on any Go version.
- Forgetting to invoke a function literal leaves you with a function value instead of its result, which the compiler will reject as a type mismatch.
