Go Anonymous Functions and Closures

Anonymous functions, also called function literals, are functions defined without a name. In Go, they're syntactically identical to regular functions except they omit the function name. You can...

Key Insights

  • Anonymous functions in Go are first-class citizens that can be assigned to variables, passed as arguments, and returned from other functions, enabling powerful functional programming patterns
  • Closures capture variables from their enclosing scope and maintain access to them even after the outer function returns, making them ideal for state preservation, middleware chains, and callback handlers
  • The loop variable capture problem is the most common closure pitfall—always pass loop variables as function parameters or use loop-scoped variables when creating goroutines or deferred functions inside loops

Introduction to Anonymous Functions

Anonymous functions, also called function literals, are functions defined without a name. In Go, they’re syntactically identical to regular functions except they omit the function name. You can assign them to variables, pass them as arguments, or invoke them immediately.

package main

import "fmt"

func main() {
    // Anonymous function assigned to a variable
    greet := func(name string) string {
        return fmt.Sprintf("Hello, %s!", name)
    }
    fmt.Println(greet("Alice"))

    // Immediately invoked function expression (IIFE)
    result := func(a, b int) int {
        return a + b
    }(5, 3)
    fmt.Println(result) // Output: 8

    // Anonymous function with no parameters
    func() {
        fmt.Println("I'm executed immediately")
    }()
}

Use anonymous functions when you need a short-lived function for a specific context, like callbacks, goroutines, or one-off operations. Use named functions when the logic is reusable, complex, or needs to be tested independently.

Anonymous Functions as First-Class Citizens

Go treats functions as first-class citizens, meaning they can be manipulated like any other value. This enables functional programming patterns that make code more flexible and expressive.

Passing functions as arguments is common in Go’s standard library. The sort.Slice() function is a perfect example:

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

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

    // Sort by age using an anonymous function
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age < people[j].Age
    })

    fmt.Println(people) // [{Bob 25} {Alice 30} {Charlie 35}]
}

Functions can return other functions, creating powerful abstractions:

func multiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := multiplier(2)
    triple := multiplier(3)

    fmt.Println(double(5))  // 10
    fmt.Println(triple(5))  // 15
}

You can also store functions in data structures:

func main() {
    operations := []func(int, int) int{
        func(a, b int) int { return a + b },
        func(a, b int) int { return a - b },
        func(a, b int) int { return a * b },
    }

    for _, op := range operations {
        fmt.Println(op(10, 5))
    }
    // Output: 15, 5, 50
}

Understanding Closures

A closure is an anonymous function that captures variables from its enclosing scope. The function maintains access to these variables even after the outer function has returned. This creates a persistent state associated with the function.

Here’s a classic counter example:

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c1 := counter()
    fmt.Println(c1()) // 1
    fmt.Println(c1()) // 2
    fmt.Println(c1()) // 3

    c2 := counter()
    fmt.Println(c2()) // 1 (separate state)
}

Each call to counter() creates a new count variable, and the returned function closes over that specific variable. Multiple closures can also share the same outer variable:

func makeCounters() (func(), func() int) {
    count := 0
    
    increment := func() {
        count++
    }
    
    get := func() int {
        return count
    }
    
    return increment, get
}

func main() {
    inc, get := makeCounters()
    inc()
    inc()
    fmt.Println(get()) // 2
}

Common Closure Patterns

Closures shine in middleware chains, a pattern heavily used in HTTP servers:

type Middleware func(http.HandlerFunc) http.HandlerFunc

func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        log.Printf("%s %s - %v", r.Method, r.URL.Path, time.Since(start))
    }
}

func authMiddleware(requiredRole string) Middleware {
    return func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            role := r.Header.Get("X-User-Role")
            if role != requiredRole {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }
            next(w, r)
        }
    }
}

func main() {
    handler := func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Success!"))
    }

    // Chain middleware
    wrapped := loggingMiddleware(authMiddleware("admin")(handler))
    http.HandleFunc("/admin", wrapped)
}

Function factories are another practical pattern:

type Config struct {
    Timeout time.Duration
    Retries int
}

func newHTTPClient(cfg Config) func(string) (*http.Response, error) {
    client := &http.Client{Timeout: cfg.Timeout}
    
    return func(url string) (*http.Response, error) {
        var resp *http.Response
        var err error
        
        for i := 0; i < cfg.Retries; i++ {
            resp, err = client.Get(url)
            if err == nil {
                return resp, nil
            }
            time.Sleep(time.Second * time.Duration(i+1))
        }
        return nil, err
    }
}

func main() {
    fetchWithRetry := newHTTPClient(Config{
        Timeout: 5 * time.Second,
        Retries: 3,
    })
    
    resp, err := fetchWithRetry("https://api.example.com/data")
    // Handle response...
}

Common Pitfalls and Gotchas

The most notorious closure bug in Go is the loop variable capture problem. This occurs when creating goroutines or closures inside loops:

// WRONG: All goroutines print the same value
func main() {
    values := []int{1, 2, 3, 4, 5}
    
    for _, v := range values {
        go func() {
            fmt.Println(v) // Captures the loop variable
        }()
    }
    
    time.Sleep(time.Second)
    // Likely output: 5 5 5 5 5
}

The problem: all goroutines capture the same v variable, which gets updated on each iteration. By the time the goroutines execute, v holds the last value.

Solution 1: Pass as parameter

for _, v := range values {
    go func(val int) {
        fmt.Println(val)
    }(v)
}

Solution 2: Loop-scoped variable (Go 1.22+)

for _, v := range values {
    v := v // Creates a new variable in each iteration
    go func() {
        fmt.Println(v)
    }()
}

As of Go 1.22, loop variables are now per-iteration by default, but understanding this pattern remains important for older codebases.

Performance Considerations

Closures have minimal overhead in most cases, but they do allocate memory to store captured variables. If you’re creating millions of closures in a tight loop, this could matter:

func BenchmarkClosure(b *testing.B) {
    multiplier := 2
    for i := 0; i < b.N; i++ {
        _ = func(x int) int {
            return x * multiplier
        }(i)
    }
}

func BenchmarkRegular(b *testing.B) {
    multiplier := 2
    for i := 0; i < b.N; i++ {
        _ = multiply(i, multiplier)
    }
}

func multiply(x, multiplier int) int {
    return x * multiplier
}

In practice, the difference is negligible unless you’re in an extremely hot path. The readability and maintainability benefits of closures usually outweigh the minimal performance cost.

Best Practices and Real-World Use Cases

Use closures with defer for guaranteed cleanup:

func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer func() {
        if err := f.Close(); err != nil {
            log.Printf("Failed to close file: %v", err)
        }
    }()
    
    // Process file...
    return nil
}

Table-driven tests benefit from closures for setup and teardown:

func TestUserService(t *testing.T) {
    tests := []struct {
        name string
        test func(*testing.T)
    }{
        {
            name: "creates user successfully",
            test: func(t *testing.T) {
                db := setupTestDB()
                defer db.Close()
                
                service := NewUserService(db)
                user, err := service.Create("alice@example.com")
                
                if err != nil {
                    t.Fatalf("unexpected error: %v", err)
                }
                if user.Email != "alice@example.com" {
                    t.Errorf("expected alice@example.com, got %s", user.Email)
                }
            },
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, tt.test)
    }
}

Closures are a powerful tool in Go’s arsenal. Use them for callbacks, middleware, state encapsulation, and functional abstractions. Avoid them when a simple named function would suffice, and always be mindful of variable capture in loops. Master closures, and you’ll write more expressive, maintainable Go code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.