Closures
Learn how Go closures capture surrounding variables, with a hands-on counter-generator example that shows why closures are useful.
Introduction
You already know that functions in Go are first-class values. Closures take that a step further: a closure is a function value that "closes over" — captures and remembers — variables from the scope in which it was created, even after that outer scope has finished executing.
- What a closure is and how variable capture works.
- How to build a stateful counter generator using a closure.
- Why each closure created from the same function has independent state.
- The classic loop-variable capture pitfall and how to avoid it.
What Is a Closure?
When you define an anonymous function inside another function, it can reference variables declared in the enclosing function. Go keeps those variables alive for as long as the closure that references them exists — even after the outer function has returned.
package main
import "fmt"
func makeGreeter(greeting string) func(string) string { // "greeting" is captured by the returned closure. return func(name string) string { return greeting + ", " + name + "!" }}
func main() { sayHello := makeGreeter("Hello") sayHowdy := makeGreeter("Howdy")
fmt.Println(sayHello("Alice")) fmt.Println(sayHowdy("Bob"))}Click Run to see what this code prints.
Even though makeGreeter has already returned by the time sayHello and sayHowdy are called, each remembers its own "greeting" value from when it was created.
A Counter Generator
The classic closure example is a counter: a function that returns another function which increments and remembers a count across calls, without exposing that count as a global or package-level variable.
package main
import "fmt"
func makeCounter() func() int { count := 0 return func() int { count++ // mutates the captured variable directly return count }}
func main() { counter := makeCounter()
fmt.Println(counter()) fmt.Println(counter()) fmt.Println(counter())}Click Run to see what this code prints.
The count variable lives on in memory, privately owned by the returned closure, for as long as that closure is reachable. There's no way for outside code to read or modify count directly — the closure is the only access point.
Each Closure Has Its Own State
Every call to makeCounter creates a brand-new count variable. Closures produced from separate calls are completely independent of each other — they do not share state.
package main
import "fmt"
func makeCounter() func() int { count := 0 return func() int { count++ return count }}
func main() { counterA := makeCounter() counterB := makeCounter()
fmt.Println(counterA()) // 1 fmt.Println(counterA()) // 2 fmt.Println(counterB()) // 1 — independent from counterA fmt.Println(counterA()) // 3}Click Run to see what this code prints.
The Classic Loop Variable Pitfall
A well-known closure gotcha involves capturing a loop variable. In Go versions before 1.22, the loop variable was reused across every iteration, so closures created inside a loop would all end up referencing the same final value unless you took extra care. Since Go 1.22, each loop iteration gets its own fresh copy of the loop variable, which fixed this pitfall at the language level — but you'll still see the defensive pattern in older codebases, and it's important to understand.
package main
import "fmt"
func main() { var printers []func()
// On Go 1.22+, each iteration has its own "i", so this now // correctly prints 0, 1, 2. On older Go versions, it printed // 3, 3, 3 because all closures shared the same "i". for i := 0; i < 3; i++ { printers = append(printers, func() { fmt.Println(i) }) }
for _, p := range printers { p() }}Click Run to see what this code prints.
This behavior changed in Go 1.22 (released early 2024). If you're maintaining code that must support older Go versions, guard against this by capturing the loop variable explicitly inside the loop body, e.g. i := i, before using it in a closure.
Common Mistakes
- Assuming closures always share state — closures created from separate calls to the same outer function are independent.
- Forgetting that captured variables are shared by reference, so mutating one closure's captured variable can be visible to others if they were captured from the same scope (not the same call).
- Relying on pre-Go-1.22 loop variable semantics without checking your project's go.mod Go version.
- Creating unnecessary closures for simple logic that would be clearer as a plain function or inline expression.
- Capturing large data structures by reference inside long-lived closures, unintentionally keeping them alive in memory longer than needed.
Best Practices
- Use closures to encapsulate private state without resorting to global or package-level variables.
- Keep closures small and purposeful — if a closure's logic grows complex, consider a named function or a small struct with methods instead.
- Check your project's Go version before relying on (or working around) the loop-variable-per-iteration behavior.
- Return closures from constructor-style functions (like makeCounter) when you want to hand callers a stateful, self-contained function.
- Be mindful of what a closure keeps alive in memory — captured variables persist as long as the closure itself is reachable.
Frequently Asked Questions
No, closures exist in many languages including JavaScript, Python, and Ruby. Go's version works the same conceptually: a function capturing variables from its enclosing scope.
Closures capture variables by reference, not by value. If the outer variable changes after the closure is created but before it's called, the closure sees the updated value.
Closures are great for small, single-purpose stateful behavior, like a counter or a configured callback. For anything with multiple related pieces of state or several operations, a struct with methods is usually clearer.
Key Takeaways
- A closure is a function value that captures variables from its enclosing scope by reference.
- Captured variables persist as long as the closure that references them is reachable.
- Each call to a function that returns a closure creates fresh, independent captured state.
- Since Go 1.22, each loop iteration has its own copy of the loop variable, avoiding the classic capture bug.
- Closures are ideal for encapsulating small pieces of private, mutable state.
Summary
Closures let you build self-contained, stateful behavior without reaching for global variables, and they're a building block you'll see again when working with callbacks and goroutines. Next, you'll learn about pointers — how Go lets you reference and mutate values directly, without the pointer arithmetic found in languages like C.