Go Named Return Values — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code involving named return values. Identify the bug, explain why, and provide the corrected code. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Naked Return Without Named Results¶
package main
import "fmt"
func half(n int) int {
result := n / 2
return // BUG
}
func main() {
fmt.Println(half(10))
}
Solution
**Bug**: Naked `return` requires named results. **Compile error**: `not enough arguments to return`. **Fix** (option A — name the result): **Fix** (option B — explicit return): **Key lesson**: Naked return is allowed only with named results.Bug 2 🟢 — Shadowing Named Result¶
package main
import "fmt"
func compute(input int) (n int) {
n := input * 2 // BUG
return
}
func main() {
fmt.Println(compute(5))
}
Solution
**Bug**: `n := input * 2` declares a NEW local `n`, shadowing the named result. The named result is never assigned. **Compile error** typically catches this: `n declared but not used`. If the compile error is suppressed somehow (or if the inner code uses the local), the named result returns 0. **Fix** — use `=` (assignment), not `:=` (declaration): Output: `10`. **Key lesson**: Use `=` to assign to named results, not `:=`. The latter shadows.Bug 3 🟢 — Forgotten Assignment¶
package main
import "fmt"
func minMax(xs []int) (min, max int) {
if len(xs) == 0 {
return
}
for _, x := range xs {
if x < min { min = x }
if x > max { max = x }
}
return
}
func main() {
fmt.Println(minMax([]int{3, 7, 1, 5}))
}
What's the bug?
Hint
What are `min` and `max` initialized to?Solution
**Bug**: `min` and `max` are initialized to 0 (zero value of int). On the first iteration, the comparisons `x < 0` and `x > 0` may not behave as expected for non-positive inputs. For `[3, 7, 1, 5]`: - Initial: min=0, max=0 - x=3: 3<0 false, 3>0 true → max=3 - x=7: 7<0 false, 7>0 true → max=7 - x=1: 1<0 false, 1>0 false → no change - x=5: 5<0 false, 5>0 false → no change - Returns (0, 7) For all-positive input, `min` is wrongly 0. **Fix** — initialize min and max to the first element: Now returns (1, 7) for the example. **Key lesson**: Named results are zero-initialized; that may not be a sensible starting state for your algorithm.Bug 4 🟢 — Defer Eager Argument Evaluation¶
package main
import "fmt"
func work() (n int) {
n = 1
defer fmt.Println("n at defer:", n) // BUG?
n = 99
return
}
func main() {
work()
}
What's printed? Did the author intend this?
Solution
**Discussion**: `defer fmt.Println(...)` evaluates arguments EAGERLY, at defer time. `n` is captured as 1. Output: If the author wanted to see the FINAL value (99), they need a closure: Output now: **Key lesson**: `defer call(args)` evaluates args eagerly. Use `defer func(){...}()` for late evaluation.Bug 5 🟡 — Mixed Named and Unnamed¶
Solution
**Bug**: You cannot mix named and unnamed in the same result list. **Compile error**: `mixed named and unnamed parameters`. **Fix** (all named): **Fix** (all unnamed): **Key lesson**: Pick one style for the result list — all named or all unnamed.Bug 6 🟡 — Defer Reads Stale Value¶
package main
import (
"errors"
"fmt"
)
func op() (err error) {
defer fmt.Println("err was:", err) // BUG
err = errors.New("failed")
return
}
func main() {
op()
}
What's printed?
Solution
**Bug**: `defer fmt.Println(...)` evaluates args eagerly. `err` is captured as `nil` (zero value). Output: **Fix** — closure: Output now: **Key lesson**: Use a closure to defer evaluation along with the call.Bug 7 🟡 — := in defer Closure Shadows¶
package main
import (
"errors"
"fmt"
)
func op() (err error) {
defer func() {
if err != nil {
err := fmt.Errorf("wrapped: %w", err) // BUG
_ = err
}
}()
return errors.New("inner")
}
func main() {
fmt.Println(op())
}
Solution
**Bug**: Inside the deferred closure, `err :=` declares a NEW local `err`, shadowing the named result. The wrapping happens but never updates the function's named `err`. Output: **Fix** — use `=` (assignment): Output now: **Key lesson**: Inside defer closures, use `=` to modify the captured named result, not `:=`.Bug 8 🟡 — Recover Without Setting Error¶
package main
import "fmt"
func safe() (err error) {
defer func() {
recover() // BUG: discards the recovered value
}()
panic("boom")
return nil
}
func main() {
err := safe()
fmt.Println(err)
}
Solution
**Bug**: `recover()` is called but its return value is discarded. The panic IS absorbed (recover was called inside a deferred function), but `err` is never set. The caller gets `nil` even though there was a panic. Output: **Fix** — capture and set err: Output now: **Key lesson**: When using recover for panic-to-error, always check the return value and assign to the named error.Bug 9 🟡 — Rollback Logic Backwards¶
package main
import (
"errors"
"fmt"
)
type Tx struct{}
func (t *Tx) Commit() error { fmt.Println("commit"); return nil }
func (t *Tx) Rollback() error { fmt.Println("rollback"); return nil }
func transfer(amount int) (err error) {
tx := &Tx{}
defer func() {
if err == nil {
tx.Rollback() // BUG
} else {
tx.Commit()
}
}()
if amount <= 0 {
err = errors.New("bad amount")
return
}
return
}
func main() {
fmt.Println(transfer(100))
}
Solution
**Bug**: The rollback/commit logic is inverted. On success (`err == nil`) we should commit; on failure rollback. For amount=100: success path; current code calls Rollback. WRONG. **Fix**: Now success commits, failure rolls back. Also captures commit errors if commit itself fails. **Key lesson**: Auto-rollback patterns require careful logic. Test both success and failure paths.Bug 10 🔴 — Closure Captures Named Return Concurrently¶
package main
import (
"fmt"
"sync"
)
func work() (n int) {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n++ // BUG
}()
}
wg.Wait()
return
}
func main() {
fmt.Println(work())
}
What's the bug?
Solution
**Bug**: 5 goroutines concurrently increment `n` (the named result) without synchronization. Data race; final value may not be 5. `go run -race main.go` flags it. **Fix** — synchronize: Or use atomic: **Key lesson**: Named results are regular variables. Concurrent mutation requires synchronization.Bug 11 🔴 — Returning Typed Nil Through Named Error¶
package main
import "fmt"
type MyErr struct{}
func (e *MyErr) Error() string { return "my error" }
func op() (err error) {
var p *MyErr // nil
err = p
return
}
func main() {
err := op()
fmt.Println("err is nil?", err == nil)
}
Solution
**Bug**: `err = p` assigns a typed nil pointer to the `error` interface. The interface holds a non-nil type word + nil data — `err == nil` returns false. Output: **Fix** — explicitly assign nil: Or just don't assign on the no-error path: **Key lesson**: Typed nil pointers wrapped in an interface produce non-nil interfaces. For named errors, prefer literal `nil` or just don't assign on success paths.Bug 12 🔴 — Deferred Modification Order¶
package main
import "fmt"
func compute() (n int) {
defer func() { n *= 2 }()
defer func() { n += 1 }()
n = 5
return
}
func main() {
fmt.Println(compute())
}
What's the output? Why?
Solution
**Output**: `12`. **Why**: 1. `n = 5` (body sets n). 2. `return` → no expression, so n stays 5. 3. Defers run in LIFO order. 4. defer registered LAST runs FIRST: `n += 1` → n = 6. 5. defer registered FIRST runs LAST: `n *= 2` → n = 12. 6. Function returns 12. If you want `(5 * 2) + 1 = 11`, swap the registration order. **Key lesson**: Defers run LIFO. Order registration carefully when modifications are sequence-dependent.Bug 13 🔴 — Mixing Defer + Goroutine Modifying Named Result¶
package main
import (
"fmt"
"time"
)
func work() (n int) {
defer func() { n = 99 }()
go func() {
time.Sleep(50 * time.Millisecond)
n = 50 // BUG: races with defer
}()
return 1
}
func main() {
n := work()
fmt.Println(n)
time.Sleep(100 * time.Millisecond) // wait for goroutine
}
What's the bug?
Solution
**Bug**: The function returns before the goroutine modifies `n`. By the time `fmt.Println(n)` runs in main, the deferred `n = 99` has already run and the function returned 99. But the spawned goroutine also writes to `n` (the named result variable, which has escaped to the heap because the goroutine captures it). After the function returns, the goroutine writes 50 to `n`. But the caller already received 99. The race is: the goroutine writes to `n` (now a heap variable) AFTER the function returns. Nothing reads `n` from main, but if the caller had a pointer to it... actually, the caller received a copy of the value 99. The real problem: **the goroutine modifies a variable that should have been local to the function call**. The function exited, the named result was captured into the return value, and the goroutine's later write is meaningless (no one reads the heap n anymore). But there's a race detector flag: the goroutine writes to `n` while main reads it via the return value. Wait, the return value IS a copy. Actually, the race might be on the heap-promoted `n` between main's deferred `n = 99` and the goroutine's `n = 50`. If they happen to overlap, race. **Fix** — don't spawn goroutines that modify named results without synchronization with the function's return. **Key lesson**: A named result captured by a goroutine that outlives the function is a recipe for races and confusion. Avoid this pattern.Bonus Bug 🔴 — Defer Closure Captures Wrong Named Result¶
package main
import "fmt"
func compute() (a int, b int) {
defer func() {
a = a * b // BUG?
}()
a = 3
b = 4
return
}
func main() {
fmt.Println(compute())
}