7.6 Anonymous Functions and Closures
Right, let’s talk about anonymous functions and closures. This is where Go starts to feel less like a rigid blueprint and more like a proper, modern programming language. It’s also where you can paint yourself into a spectacularly confusing corner if you’re not careful. I’m here to make sure you use the paint, not wear it.
An anonymous function is exactly what it sounds like: a function without a name. You declare it right where you need it, which is incredibly useful for short, one-off jobs. The most straightforward use is assigning it to a variable.
// Assigning an anonymous function to a variable
greet := func(name string) {
fmt.Printf("Hello, %s!\n", name)
}
greet("Gopher") // Output: Hello, Gopher!
Notice the syntax? The func keyword is immediately followed by the parameters. There’s no name sandwiched in between. This greet variable isn’t a string or an integer; it’s a function value. This is a first-class citizen in Go, meaning you can pass it around, return it from other functions, and generally treat it like any other value.
But where this gets truly powerful, and where most of the head-scratching begins, is when we combine anonymous functions with closures. A closure is a function that closes over or captures variables from its surrounding scope. The function retains access to those variables even after the surrounding function has finished executing. It’s like the function carries a little backpack of the context it was created in.
The Classic (and Slightly Contrived) Counter
Let’s look at a canonical example. You want a counter that increments each time you call it. Without a closure, you’d need a package-level variable, which is gross. With a closure, you can encapsulate that state beautifully.
func newCounter() func() int {
count := 0 // This variable is captured by the closure below
return func() int {
count++
return count
}
}
func main() {
counter := newCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// A new closure, a new captured variable!
anotherCounter := newCounter()
fmt.Println(anotherCounter()) // 1
}
Here’s the magic: the count variable doesn’t die when newCounter returns. It lives on, bound to the specific function instance that counter points to. Each call to newCounter creates a new count variable and a new function that closes over it. This is how you get private, encapsulated state in Go without any classes or objects.
The Gotcha: Loop Variable Capture
This is the big one. The thing that has caused more bugs than I’ve had hot dinners. Pay attention, because you will run into this.
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // What does this print?
})
}
for _, f := range funcs {
f()
}
}
What do you think this outputs? 0, 1, 2? Oh, my sweet summer child. It prints 3, 3, 3. Why? Because all three anonymous functions are closures that capture the same variable i. By the time we get around to executing them, the loop has finished and i is equal to 3. We have three functions all pointing to the same memory location.
The fix is to break the closure by passing the value as a parameter. Since function parameters are passed by value, each iteration gets its own copy.
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
j := i // Create a new variable scoped to this iteration
funcs = append(funcs, func() {
fmt.Println(j) // Captures 'j', which is a different variable each time
})
}
for _, f := range funcs {
f() // Output: 0, 1, 2
}
}
Alternatively, you can be more explicit by defining the function to take the value as an argument right in the loop.
for i := 0; i < 3; i++ {
func(n int) { // Define a function that takes an integer
funcs = append(funcs, func() {
fmt.Println(n) // Now it captures 'n', not 'i'
})
}(i) // Immediately invoke it, passing the current value of i
}
This isn’t a bug in Go; it’s a fundamental behavior of how closures work. But it’s a design choice that consistently trips people up. The language could have been designed to capture the value of the variable at the time the closure was created, but it doesn’t. It captures a reference to the variable itself. You need to be aware of it.
Immediately Invoked Function Expressions (IIFEs)
Sometimes you just want to create a little isolated scope for something, right then and there. You can declare an anonymous function and call it immediately. This is great for things like initialization or scoping a defer.
func initDB() error {
// This 'conn' variable is scoped only within this IIFE
conn, err := connectToDatabase()
if err != nil {
return err
}
defer conn.Close() // This defer runs at the end of the IIFE, not initDB
// Do some one-time setup with the connection
return createTables(conn)
}() // <-- The parentheses here invoke the function immediately
// Note: The real function signature would be: func() error { ... }()
Practical Uses Beyond Academia
So besides confusing interview candidates, what are closures good for? Everything.
- Middleware: Passing a
http.Handlerto a function that returns a newhttp.Handleris a classic closure pattern. The returned handler closes over any configuration for that middleware. - Deferring Work: Launching a goroutine that runs a function which closes over the current state.Be careful here too! If you’re in a loop, you’re back to the loop variable capture problem. Always pass the data you need as parameters to the goroutine.
func process(data []byte) { // Launch a goroutine that closes over the data to log it later go func() { time.Sleep(1 * time.Second) fmt.Printf("Processed: %s\n", string(data)) }() }
Closures are one of Go’s superpowers. They let you write expressive, concise, and powerful code. Respect them, understand how they capture variables, and you’ll unlock a whole new level of programming in Go. Underestimate them, and they will exact their revenge in the most subtle and frustrating ways possible.