Function Literals & Closures
A function literal is an anonymous function expression you can define and invoke inline.
Search across all documentation pages
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.
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.
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:
defer needs a local function adjusting cleanup behavior.slices.SortFunc / sort.Slice comparators.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:
New returns a closure hiding mu and last.Limiter.go statement schedules the closure - capture semantics matter at schedule time, not only at definition time.| 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 |
| 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 |
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.
v := v or pass as parameter.func(){ defer cleanup(); work() }().func() panics. Fix: guard or require constructor.&v in loop shares one address. Fix: copy value inside loop body before taking address.Add before go races. Fix: call Add before launch; use defer Done() inside goroutine.| 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 |
A function literal is syntax for an anonymous function.
It becomes a closure when it references variables from an outer scope.
Variables are captured by reference.
Mutations visible to all closures sharing that binding unless you shadow per iteration.
Per-iteration loop variables reduce the classic goroutine capture bug.
Still prefer explicit parameters in libraries targeting older releases or when clarity helps readers.
Yes - returning a closure from a factory extends lifetime of captured variables on the heap.
http.HandlerFunc is a function type with a ServeHTTP method.
Literals convert to handlers: http.HandlerFunc(func(w,r){...}).
Captured values live as long as the closure value exists.
Drop references to closures to release retained state.
Yes - literals can appear inside generic functions respecting type parameters.
Signature must match required func type exactly.
Use httptest.ResponseRecorder and a stub next handler recording calls.
Extract middleware constructor to named function for table tests.
Deferred functions in closures run on surrounding function exit.
Separate rules apply for panic/recover boundaries - keep recover at goroutine entry when needed.
When behavior is a single hook (Limiter, Validator, AttemptFunc).
Switch to interfaces when mocks need multiple methods.
Long-lived handlers capturing request-scoped large buffers can leak.
Capture IDs, not whole requests.
govet and staticcheck flag defer in loop and capture issues.
Run linters in CI on concurrent code.
HandlerFunc method patternStack 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).
Reviewed by Chris St. John·Last updated Jul 18, 2026