Function Literals & Closures
A function literal is an anonymous function expression you can define and invoke inline.
When it references variables from surrounding scopes, it becomes a closure that carries those bindings into later execution - including goroutines and deferred calls.
Summary
Function literals enable local callbacks, middleware, and factories without declaring named top-level functions.
Closures capture variables by reference, not by snapshot, unless you copy loop variables or pass parameters explicitly.
The classic goroutine bug launches multiple closures sharing one loop variable.
Factories return configured closures (rate limiters, validators) while hiding mutable state in the outer function.
Use literals for short, local behavior; promote to named functions when logic grows or needs testing in isolation.
Recipe
Quick-reference recipe card - copy-paste ready.
func withTimeout(parent context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, d)
}
func middleware(log func(string)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log(r.Method + " " + r.URL.Path)
next.ServeHTTP(w, r)
})
}
}
// Safe goroutine spawn in a loop.
for _, id := range ids {
id := id
go func() {
process(id)
}()
}When to reach for this:
- HTTP/gRPC middleware wrapping handlers.
deferneeds a local function adjusting cleanup behavior.- Sorting with
slices.SortFunc/sort.Slicecomparators. - Tests supply inline stubs implementing small interfaces.
Working Example
package ratelimit
import (
"context"
"sync"
"time"
)
type Limiter func(ctx context.Context) error
func New(interval time.Duration) Limiter {
var mu sync.Mutex
var last time.Time
return func(ctx context.Context) error {
mu.Lock()
defer mu.Unlock()
now := time.Now()
wait := interval - now.Sub(last)
if wait > 0 {
timer := time.NewTimer(wait)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
}
}
last = time.Now()
return nil
}
}
func Example(ctx context.Context) error {
wait := New(100 * time.Millisecond)
for i := 0; i < 3; i++ {
if err := wait(ctx); err != nil {
return err
}
}
return nil
}What this demonstrates:
- Factory
Newreturns a closure hidingmuandlast. - Captured state serializes callers without exporting a struct.
- Context respected during waits inside the closure.
- Mutex protects shared variables across goroutines invoking the same
Limiter.
Deep Dive
How It Works
- Function literals compile to heap-allocated function values when they escape (stored, returned, sent to goroutines).
- Free variables are captured by reference through hidden pointers in the closure struct.
- Named functions can close over package-level vars; literals commonly close over locals in the enclosing function.
gostatement schedules the closure - capture semantics matter at schedule time, not only at definition time.
Capture Pitfalls and Fixes
| Scenario | Problem | Fix |
|---|---|---|
for loop var | All goroutines see final value | id := id or pass param go func(id int){...}(id) |
defer in loop | Defers stack until function returns | Wrap body in literal func(){ defer ... }() |
| Mutated capture | Races on shared vars | Mutex or channel serialization |
| Large captured struct | Accidental retention | Capture pointer to needed field only |
Factory vs Struct
| Pattern | Strength | Weakness |
|---|---|---|
| Closure factory | Minimal API surface | Harder to inspect state in tests |
| Struct with methods | Clear fields, interfaces | More boilerplate |
| Function type alias | Composable middleware | No multiple methods |
Go Notes
sort.Slice(items, func(i, j int) bool {
return items[i].Priority < items[j].Priority
})Literals excel for one-off comparators and filters.
Extract when reused across packages.
Gotchas
- Loop variable capture in goroutines - Classic data race on loop index/item. Fix: shadow with
v := vor pass as parameter. - Defer in loop without closure wrapper - All defers run at outer function end, not per iteration. Fix:
func(){ defer cleanup(); work() }(). - Nil closure invocation - Assigning uninitialized
func()panics. Fix: guard or require constructor. - Capturing pointers to loop variables -
&vin loop shares one address. Fix: copy value inside loop body before taking address. - Closure retains large objects - Prevents GC of captured maps/slices longer than needed. Fix: narrow capture to IDs or interfaces.
- Testing anonymous logic - Hard to target without exporting. Fix: name function or inject interface when complexity grows.
- sync.WaitGroup + goroutine closure - Forgetting
Addbeforegoraces. Fix: callAddbefore launch; usedefer Done()inside goroutine.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Named function | Reused, needs direct unit test | One-liner comparator |
| Interface implementation | Multiple methods, mocks | Single hook |
| Struct + method | Stateful service with clear API | Tiny one-off callback |
| Channels for coordination | Pipeline stages | Simple serial limiter suffices |
FAQs
What is the difference between a function literal and a closure?
A function literal is syntax for an anonymous function.
It becomes a closure when it references variables from an outer scope.
Are captured variables copied?
Variables are captured by reference.
Mutations visible to all closures sharing that binding unless you shadow per iteration.
Why does Go 1.22+ change loop variable semantics?
Per-iteration loop variables reduce the classic goroutine capture bug.
Still prefer explicit parameters in libraries targeting older releases or when clarity helps readers.
Can closures outlive the enclosing function?
Yes - returning a closure from a factory extends lifetime of captured variables on the heap.
How do literals relate to http.HandlerFunc?
http.HandlerFunc is a function type with a ServeHTTP method.
Literals convert to handlers: http.HandlerFunc(func(w,r){...}).
Are closures garbage-collected with captures?
Captured values live as long as the closure value exists.
Drop references to closures to release retained state.
Can I use function literals with generics?
Yes - literals can appear inside generic functions respecting type parameters.
Signature must match required func type exactly.
How do I test middleware built from literals?
Use httptest.ResponseRecorder and a stub next handler recording calls.
Extract middleware constructor to named function for table tests.
Do closures work with defer and panic?
Deferred functions in closures run on surrounding function exit.
Separate rules apply for panic/recover boundaries - keep recover at goroutine entry when needed.
When should factories return func types?
When behavior is a single hook (Limiter, Validator, AttemptFunc).
Switch to interfaces when mocks need multiple methods.
Can closures cause memory leaks?
Long-lived handlers capturing request-scoped large buffers can leak.
Capture IDs, not whole requests.
How does golangci-lint help?
govet and staticcheck flag defer in loop and capture issues.
Run linters in CI on concurrent code.
Related
- Variadic Functions & Function Types - naming function types and middleware
- Functions & Methods Basics - safe loop capture example
- Methods: Value vs Pointer Receivers -
HandlerFuncmethod pattern - Functions and Interfaces: Go's Composition Model - composing behavior with functions
- Functions, Methods & Interfaces Best Practices - goroutine and middleware guidance
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).