Go Anonymous Functions — Middle Level¶
1. Introduction¶
At the middle level you treat anonymous functions as first-class values with capture semantics: tools for inline composition, scoped initialization, dependency injection, and lifecycle hooks. You also recognize when to keep them anonymous vs extract to named functions for readability and stack-trace clarity.
2. Prerequisites¶
- Junior-level anonymous function material
- Basic understanding of closures (deeper coverage in 2.6.5)
- Goroutines and
syncpackage - Understanding of
defer/panic/recover
3. Glossary¶
| Term | Definition |
|---|---|
| Function literal | Go's official term for anonymous function |
| Closure | Function literal that captures enclosing variables |
| Funcval | Runtime representation: code pointer + optional captures |
| IIFE | Immediately-Invoked Function Expression |
| Decorator | Higher-order function returning a wrapped version of its input |
| Middleware | Decorator pattern applied to HTTP handlers / RPC calls |
| Hook | Optional function value used to extend behavior |
4. Core Concepts¶
4.1 Function Literal vs Named Function¶
Same type-system semantics, different ergonomics:
// Named — appears in stack traces, can be tested directly
func double(x int) int { return x * 2 }
// Anonymous — convenient inline, but stack frame shows "func1"
double := func(x int) int { return x * 2 }
Stack traces: - Named: main.double(...) — clear. - Anonymous: main.main.func1(...) — generic.
For library-grade code, named is friendlier to debuggers and profilers.
4.2 Capture Semantics — By Reference¶
Variables captured by a function literal are shared with the enclosing scope:
This means: - Changes outside the closure are visible inside. - Changes inside the closure are visible outside. - The closure keeps captured variables alive (heap-allocated if it escapes).
4.3 IIFE for Scoped Initialization¶
config := func() *Config {
raw, err := os.ReadFile("config.json")
if err != nil { log.Fatal(err) }
var c Config
if err := json.Unmarshal(raw, &c); err != nil { log.Fatal(err) }
return &c
}()
The temporary variables raw and err are scoped to the IIFE. Useful when initialization is multi-step but you don't want a named helper.
4.4 Decorator Pattern¶
A decorator is a function that takes a function and returns a wrapped version:
func logged(fn func(int) int) func(int) int {
return func(x int) int {
fmt.Println("calling with", x)
result := fn(x)
fmt.Println("returned", result)
return result
}
}
double := func(x int) int { return x * 2 }
loggedDouble := logged(double)
loggedDouble(5)
// Output:
// calling with 5
// returned 10
// 10
4.5 Loop-Variable Capture in Go 1.22+¶
The Go 1.22 change applies to ALL three for-loop forms. Each iteration creates a fresh variable for any iteration variable declared in the for statement.
// Go 1.22+
fns := make([]func() int, 0)
for i := 0; i < 3; i++ {
fns = append(fns, func() int { return i })
}
for _, f := range fns {
fmt.Println(f()) // 0, 1, 2 in Go 1.22+
}
For Go ≤ 1.21, the same code prints 3, 3, 3. The fix is to shadow i := i inside the loop body or pass as argument.
4.6 Function Literals as Struct Fields¶
type Service struct {
OnStart func() error
OnStop func() error
}
s := Service{
OnStart: func() error {
fmt.Println("starting")
return nil
},
OnStop: func() error {
fmt.Println("stopping")
return nil
},
}
if err := s.OnStart(); err != nil { /* handle */ }
Used for hooks, callbacks, and configuration.
5. Real-World Analogies¶
An on-the-fly contractor: hire someone for one specific task; you don't put them on payroll. Anonymous = no permanent role.
Ad-hoc team formation: closures are like project teams that share state for the duration of one project, then disband.
6. Mental Models¶
Model 1 — Funcval as a tiny object¶
A function literal at runtime is a small struct:
The runtime accesses captures via a "context" register (DX on amd64).
Model 2 — Closure as state machine¶
A counter closure is a tiny state machine:
Each closure instance has its own n. The closure value bundles state + behavior — like a tiny OOP object.
7. Pros & Cons¶
Pros¶
- Inline composition — read top-to-bottom
- Closures capture context naturally
- IIFE for scoped initialization
- Avoid one-off named functions cluttering the package
Cons¶
- Less informative stack traces
- Harder to test in isolation
- Capture-by-reference can introduce subtle bugs
- Easy to abuse — long literals hurt readability
8. Use Cases¶
- Sort/filter/map callbacks
defer + recovercleanup- Goroutine bodies
- Functional options
- Decorators (logging, auth, retry)
- Hooks and lifecycle callbacks
- IIFE for scoped init
- Generators / counters via closures
- Iterator/visitor pattern
9. Code Examples¶
Example 1 — Decorator¶
package main
import "fmt"
func memoize(fn func(int) int) func(int) int {
cache := map[int]int{}
return func(x int) int {
if v, ok := cache[x]; ok { return v }
v := fn(x)
cache[x] = v
return v
}
}
var calls int
func slowDouble(x int) int { calls++; return x * 2 }
func main() {
fast := memoize(slowDouble)
fmt.Println(fast(5), fast(5), fast(5)) // 10 10 10
fmt.Println("actual calls:", calls) // 1
}
Example 2 — Functional Options¶
package main
import "fmt"
type Server struct {
Addr string
Port int
}
type Option func(*Server)
func WithAddr(a string) Option { return func(s *Server) { s.Addr = a } }
func WithPort(p int) Option { return func(s *Server) { s.Port = p } }
func NewServer(opts ...Option) *Server {
s := &Server{Addr: "localhost", Port: 8080}
for _, o := range opts { o(s) }
return s
}
func main() {
s := NewServer(WithPort(9000))
fmt.Printf("%+v\n", s)
}
Example 3 — Pipeline¶
package main
import (
"fmt"
"strings"
)
func main() {
pipeline := []func(string) string{
strings.TrimSpace,
strings.ToLower,
func(s string) string { return strings.ReplaceAll(s, " ", "-") },
}
s := " Hello World "
for _, step := range pipeline {
s = step(s)
}
fmt.Println(s) // hello-world
}
Example 4 — Custom Iterator¶
package main
import "fmt"
type Visitor func(int) bool // returns true to continue
func walk(items []int, visit Visitor) {
for _, x := range items {
if !visit(x) {
return
}
}
}
func main() {
walk([]int{1, 2, 3, 4, 5}, func(x int) bool {
if x > 3 { return false } // stop
fmt.Println(x)
return true
})
}
Example 5 — Once-Per-Goroutine Init¶
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
expensive := func() string {
fmt.Println("computing")
return "result"
}
var result string
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() { result = expensive() })
}()
}
wg.Wait()
fmt.Println(result) // computed once
}
10. Coding Patterns¶
Pattern 1 — Decorator¶
Pattern 2 — Higher-Order Reducer¶
func reduce[T, R any](xs []T, init R, fn func(R, T) R) R {
acc := init
for _, x := range xs {
acc = fn(acc, x)
}
return acc
}
sum := reduce([]int{1,2,3}, 0, func(a, b int) int { return a + b })
Pattern 3 — Visitor¶
func eachField(data map[string]any, visit func(k string, v any)) {
for k, v := range data {
visit(k, v)
}
}
Pattern 4 — Lifecycle Hook¶
type Hooks struct {
OnStart, OnStop func() error
}
func defaultHook() error { return nil }
hooks := Hooks{OnStart: defaultHook, OnStop: defaultHook}
Pattern 5 — IIFE for Multi-Step Init¶
11. Clean Code Guidelines¶
- Inline literals up to ~10 lines. Beyond that, extract to a named function.
- Avoid 3+ levels of nested literals.
- Name important callbacks so stack traces are meaningful.
- Pass loop variables explicitly in goroutines for clarity (even with Go 1.22+ semantics).
- Don't use IIFE to fake
do { ... } while— use a clear loop instead.
12. Product Use / Feature Example¶
Pluggable retry policy:
package main
import (
"errors"
"fmt"
"time"
)
type Policy func(attempt int) (sleep time.Duration, retry bool)
func retry(fn func() error, p Policy) error {
for attempt := 0; ; attempt++ {
err := fn()
if err == nil { return nil }
sleep, ok := p(attempt)
if !ok {
return fmt.Errorf("retry exhausted after %d: %w", attempt, err)
}
time.Sleep(sleep)
}
}
func main() {
var calls int
err := retry(func() error {
calls++
if calls < 3 {
return errors.New("flake")
}
return nil
}, func(attempt int) (time.Duration, bool) {
if attempt >= 5 { return 0, false }
return 5 * time.Millisecond, true
})
fmt.Println(err, calls)
}
The retry function and policy are both anonymous, defined where they're used.
13. Error Handling¶
Anonymous functions in defer handle panics:
func work() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
panic("boom")
}
Convert panics to errors at API boundaries — common pattern in stdlib (e.g., regexp.MustCompile is the panic-version; regexp.Compile is the error-version).
14. Security Considerations¶
- Closures capture sensitive data. Be careful when passing them to log handlers or untrusted code.
- Goroutines spawned with anonymous bodies share captured state — synchronize or copy.
- IIFE doesn't isolate state like a separate goroutine — it's just a scoped function.
15. Performance Tips¶
- Capture-free literals are zero-cost — same as named functions.
- Capture-with-escape literals heap-allocate — measure with
-gcflags="-m". - Lift literals out of hot loops to avoid per-iteration allocation when captures are constant.
- Indirect calls through function values prevent inlining — use named direct calls in tight inner loops where possible.
16. Metrics & Analytics¶
import "time"
func instrumented(name string, fn func()) {
start := time.Now()
fn()
fmt.Printf("[%s] %v\n", name, time.Since(start))
}
instrumented("compute", func() {
// ... work ...
})
17. Best Practices¶
- Keep literals short.
- Name them when reused or stack-trace clarity matters.
- Use the
defer func() {...}()pattern for cleanup. - Pass loop variables as args to goroutines.
- Use IIFE only for clear scoped initialization.
- Don't return literals that capture massive state — extract only what's needed.
- Profile escape behavior with
go build -gcflags="-m".
18. Edge Cases & Pitfalls¶
Pitfall 1 — Forgetting to Invoke Defer¶
defer func() { /* ... */ }
// BUG: defers a function value, never calls it
defer func() { /* ... */ }()
// CORRECT
Pitfall 2 — Loop Variable in Pre-1.22¶
Pitfall 3 — IIFE Without Result Use¶
Pitfall 4 — Heavy Capture¶
func makeWorker(big *BigStruct) func() {
return func() {
_ = big.someField
}
}
// big is alive as long as the worker is — even if you only use one field
Fix: extract just what you need:
func makeWorker(big *BigStruct) func() {
field := big.someField // capture only the small piece
return func() {
_ = field
}
}
19. Common Mistakes¶
| Mistake | Fix |
|---|---|
defer func(){} (no parens) | Add () |
| Goroutine captures shared loop var | Pass as arg or shadow |
| Trying to recurse anonymously | Use var f func(...); f = ... |
| Long anonymous body hurting readability | Extract named function |
| Capturing more than needed | Extract narrow values first |
20. Common Misconceptions¶
Misconception 1: "Anonymous functions are inherently slower." Truth: Without captures, identical to named. With captures that don't escape, stack-allocated. Heap only when escaping.
Misconception 2: "IIFE is a Go idiom." Truth: Go's package-level scoping makes IIFE less necessary than in JavaScript. Use only when scope matters.
Misconception 3: "All closures heap-allocate." Truth: Only escaping closures. The compiler stack-allocates when it can prove the closure doesn't outlive the function.
Misconception 4: "I should always use named functions for clarity." Truth: For one-line callbacks (sort comparators, filter predicates), inline is more readable than a separate named helper.
21. Tricky Points¶
func() {}is a valid expression (no-op function value).- Function literals can be variadic, generic-ish (no type params, but accept
any), and have named returns. - The runtime cost of a non-capturing literal is zero.
defer func(){}()is the CALL form;defer func(){}is a syntax error.- Closures captured by goroutines need synchronization for mutated state.
22. Test¶
package main
import "testing"
func TestDecorator(t *testing.T) {
var called int
timed := func(fn func()) func() {
return func() {
called++
fn()
}
}
counter := timed(func() {})
counter()
counter()
if called != 2 {
t.Errorf("called=%d, want 2", called)
}
}
23. Tricky Questions¶
Q1: What does this print in Go 1.22?
fns := []func() int{}
for i := 0; i < 3; i++ {
fns = append(fns, func() int { return i })
}
for _, f := range fns { fmt.Print(f(), " ") }
0 1 2 in Go 1.22+. Pre-1.22 prints 3 3 3. Q2: What is the type of f here?
func() — a function with no parameters and no results. Q3: Why does this fail to compile?
A: Inside the literal,fact is undefined (the := introduces it AFTER the right side is type-checked). Use var fact func(int) int; fact = func(...) {...}. 24. Cheat Sheet¶
// Forms
f := func(x int) int { return x }
g(func(x int) int { return x })
return func(x int) int { return x }
result := func(x int) int { return x }(5)
// In defer
defer func() { /* cleanup */ }()
// In goroutine
go func(arg T) { /* ... */ }(value)
// Recursion
var f func(int) int
f = func(n int) int { ... f(n-1) ... }
// Decorator
func wrap(fn func()) func() {
return func() { /* ... */; fn(); /* ... */ }
}
25. Self-Assessment Checklist¶
- I can write inline anonymous functions in all common positions
- I know when to extract to a named function
- I understand capture-by-reference semantics
- I know the loop-variable capture rules (pre-1.22 vs 1.22+)
- I can implement the decorator pattern
- I use IIFE for scoped initialization correctly
- I avoid heavy captures in returned closures
- I always invoke defers with
() - I know the recursion-by-name workaround
26. Summary¶
Anonymous functions are first-class values with closure semantics. They shine for one-off callbacks, decorators, and inline goroutine bodies. Watch for capture-by-reference subtleties, especially with loop variables. Extract to named functions when readability suffers. Use IIFE deliberately for scoped initialization. The cost is zero without captures, modest with stack-allocated captures, and a heap allocation when captures escape.
27. What You Can Build¶
- Decorators for logging, auth, rate-limiting
- Functional-option constructors
- Custom iterators via visitor pattern
- Pluggable retry policies
- Lifecycle hooks
- Memoization wrappers
- Test fakes via inline implementations
28. Further Reading¶
- Effective Go — Functions
- Go Blog — Defer, Panic, and Recover
- Go 1.22 release notes — loop variable change
- Dave Cheney — Functional options for friendly APIs
29. Related Topics¶
- 2.6.5 Closures (deeper coverage)
- 2.6.7 Call by Value
- Chapter 7 Concurrency
- 2.5 Loops (Go 1.22 semantics)
30. Diagrams & Visual Aids¶
Anatomy of a closure value¶
funcval {
code: &lit_body ← code pointer
captures: ← inline or pointer to capture struct
x: 5 ← captured locals
y: ptr to outer's z
}