Go Named Return Values — Optimize¶
Instructions¶
Each exercise presents inefficient or wasteful patterns around named return values. Identify the issue, write an optimized version, and explain. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Exercise 1 🟢 — Defer Eager Argument Evaluation¶
Problem:
func op() (n int, err error) {
defer fmt.Printf("op: n=%d err=%v\n", n, err) // BUG?
n = 42
err = errors.New("nope")
return
}
Question: Does this print the final values? How do you fix?
Solution
**Issue**: `defer fmt.Printf(...)` evaluates `n` and `err` EAGERLY. They're captured at defer time (n=0, err=nil). The actual print at function exit shows the captured values, not the final. Output: **Fix** — wrap in closure for late evaluation: Output now: **Performance**: closure form is slightly slower (~1 ns) but correctness matters more. **Key insight**: `defer call(args)` is eager-args. Use `defer func(){...}()` for late evaluation.Exercise 2 🟢 — Naked Return in Long Function¶
Problem:
func process(data []byte) (count int, total int, err error) {
// ... 50 lines of complex logic ...
if condition1 {
// ...
return
}
if condition2 {
// ...
return
}
// ...
return
}
Question: Why is this hard to maintain?
Solution
**Issue**: Naked returns 50 lines down from the result names obscure what's being returned. Multiple naked returns on different paths require the reader to trace state through the entire function to understand what each return produces. **Optimization** — explicit returns or split into helpers: Or split: **Key insight**: Naked return is a tool for SHORT functions. For long ones, explicit returns or function decomposition wins.Exercise 3 🟡 — Cleanup Error Capture Without Named Return¶
Problem:
func op() error {
f, err := os.Open(path)
if err != nil { return err }
var workErr error
// ... do work, set workErr ...
cerr := f.Close()
if workErr != nil { return workErr }
if cerr != nil { return cerr }
return nil
}
Question: How do you simplify with named return?
Solution
**Optimization** — named return + defer: Benefits: - Cleanup happens even on early-error paths (defer always runs). - Single source of truth: `err`. - Shorter code. - Correct even if `Close()` is called from multiple paths. **Performance**: identical to manual version (open-coded defer makes it free). **Key insight**: Named return + defer simplifies cleanup-error propagation. Use it for any function that acquires a resource.Exercise 4 🟡 — Defer Allocation in Hot Loop¶
Problem:
func processBatch(items []Item) (count int, err error) {
for _, item := range items {
defer func() {
count++
}() // BUG: defer in loop!
item.Process()
}
return
}
Question: What's wrong, and how do you fix?
Solution
**Issue**: Defer in a loop accumulates 1 defer per iteration. For 10k items, 10k defer records. Plus, defers all run at function exit, not iteration exit — `count` is incremented N times at the end, not during processing. Open-coded defer is disabled inside loops; falls back to stack/heap defer (~30-50 ns per defer). **Fix** — increment directly, not via defer: Or extract per-iteration helper if cleanup IS needed: **Benchmark** (1M items): - Defer in loop: ~80 ms, 10M defer records - Direct increment: ~20 ms, 0 defers **Key insight**: Defer doesn't run per iteration — it runs at function exit. Don't use defer for per-iteration logic.Exercise 5 🟡 — Named Return Escapes to Heap¶
Problem:
Question: What's the cost?
Solution
**Issue**: Taking the address of `n` and storing it in a global causes `n` to escape to the heap. Each call allocates a new int on the heap. Verify: **Optimization** — if you don't need persistent storage, return the value: Same escape issue at the caller, but at least it's explicit. **If you genuinely need a heap-allocated int**: The escape is now intentional. **Benchmark** (1M calls): - Heap escape via named return: ~20 ns/op, 8 B/op, 1 alloc/op - Value return: ~0.5 ns/op, 0 allocs/op (inlined) **Key insight**: Named results behave like locals. If their address escapes, they go to the heap. Avoid taking addresses unless necessary.Exercise 6 🟡 — Recover Per Iteration¶
Problem:
func processAll(items []Item) (failed int) {
for _, item := range items {
defer func() {
if r := recover(); r != nil {
failed++
}
}() // BUG: defer in loop AND eager recover semantics
item.Process()
}
return
}
Question: Two bugs. Identify and fix.
Solution
**Bugs**: 1. defer in loop accumulates; runs at function exit, not per iteration. 2. recover only works inside a deferred function — and only if a panic IS in progress at the time the defer runs. Defers all run at exit; if any item panicked, the panic propagated up immediately and didn't wait for the loop to finish. So this code DOESN'T work as a per-iteration safety net. **Fix** — extract per-iteration helper: Now each iteration has its own defer scope. A panic in `safeProcess` is recovered locally; the loop continues. **Benchmark** (1k items, 10% panic): - Buggy: panics on first failure; loop ends. - Fixed: completes all 1000, reports 100 failures. **Key insight**: Recover only catches panics in the SAME function's deferred chain. For per-iteration recovery, extract a helper.Exercise 7 🔴 — Open-Coded Defer Disabled¶
Problem:
func op() (err error) {
defer logCleanup()
defer cleanup1()
defer cleanup2()
defer cleanup3()
defer cleanup4()
defer cleanup5()
defer cleanup6()
defer cleanup7()
defer cleanup8()
defer cleanup9() // BUG: 9 defers, exceeds open-coded limit
// ...
return nil
}
Question: What's the cost difference?
Solution
**Issue**: Open-coded defer is limited to 8 defers per function. With 9, the compiler falls back to stack-allocated defers (~30 ns each). For each call: - Open-coded (≤ 8 defers): ~1 ns total overhead. - Stack defer (9+ defers): ~270 ns total. **Fix** (option A — combine cleanups): Now 1 defer; open-coded. **Fix** (option B — use a cleanup struct): Single defer, manages many cleanups manually. **Benchmark** (1M calls): - 9 defers (stack-allocated): ~270 ns/op - 1 combined defer (open-coded): ~5 ns/op **Key insight**: Open-coded defer has an 8-defer limit. Beyond that, performance drops. Combine into a single defer for hot functions.Exercise 8 🔴 — Naming Adds Noise Without Value¶
Problem:
Question: Is the named return helping?
Solution
**Discussion**: For a one-line function returning a single value, named return adds noise: - The signature is more complex. - The body has an unnecessary intermediate assignment. **Optimization** — drop the name: Same compiled code (after inlining); cleaner source. **When to keep the name**: - Defer modifies it. - The name documents non-obvious meaning (e.g., `(stddev float64)` rather than `float64`). **Key insight**: Named returns aren't free in cognitive load. Skip them when they add no value.Exercise 9 🔴 — Wrap Errors at Boundary, Not Per Step¶
Problem:
func multiStep() (err error) {
if err = step1(); err != nil {
return fmt.Errorf("step1: %w", err)
}
if err = step2(); err != nil {
return fmt.Errorf("step2: %w", err)
}
if err = step3(); err != nil {
return fmt.Errorf("step3: %w", err)
}
return
}
Question: Is wrapping at every step necessary?
Solution
**Discussion**: If callers will use `errors.Is`/`errors.As`, the wrapping helps trace where errors originated. If callers only log, the wrapping adds context but allocates ~1 wrap per error. **Optimization** (when wrapping is critical) — keep as is. **Optimization** (when wrapping is overhead) — single wrap at the boundary:func multiStep() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("multiStep: %w", err)
}
}()
if err = step1(); err != nil { return }
if err = step2(); err != nil { return }
if err = step3(); err != nil { return }
return
}
Exercise 10 🔴 — Verify Named Return Doesn't Allocate¶
Problem: You wrote a hot path with named returns and want to verify zero allocations.
var ErrEmpty = errors.New("empty")
func parse(s string) (n int, err error) {
if s == "" {
err = ErrEmpty
return
}
n = len(s)
return
}
Task: Show how to verify zero allocations on both success and failure paths.
Solution
**Step 1 — escape analysis**: Look for the absence of "moved to heap" for `n` or `err`. **Step 2 — benchmark with `-benchmem`**:func BenchmarkParseSuccess(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = parse("hello")
}
}
func BenchmarkParseError(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = parse("")
}
}