# 10 — Gotchas: "What Does This Print?" The trivia round. Each subfolder is a tiny program. The drill: 1. Open `main.go`, **predict the exact output** (or panic/deadlock/compile error). 2. `go run ./10-gotchas/exNN-.../` and check yourself. 3. Read the answer below — then explain it OUT LOUD in one breath, because that's the actual interview skill. Note: ex11 intentionally does not compile, and ex02 intentionally deadlocks — that IS the exercise. (This folder is excluded from `go build ./...` sanity by being all `main` packages; run them individually.) --- ## Answers (no peeking before predicting)
ex01 — loop variable capture Go ≥1.22: `0 1 2` in some order — the `for` statement gives each iteration a fresh `i` (spec change in 1.22, one of the very few semantic changes ever made to Go). Pre-1.22: typically `3 3 3` — all closures captured the *same* variable, which was 3 by the time they ran. Old fixes: `go func(i int){...}(i)` or shadowing `i := i`. Always mention the version boundary; it proves your knowledge has a timestamp.
ex02 — unbuffered send deadlock `fatal error: all goroutines are asleep - deadlock!` An unbuffered send blocks until a receiver is ready; there is no other goroutine. Fix A: `make(chan int, 1)`. Fix B: do the send or receive in another goroutine. Key nuance: the runtime detects only *global* deadlock (every goroutine blocked). One stuck goroutine among many = silent leak, no error, ever.
ex03 — closed channel semantics Prints `1`, then `0 false`, then **panics**: `close of closed channel`. Buffered values survive close and drain normally; after drain, receive returns zero value + `ok=false` *immediately* (never blocks). Send on closed also panics. Recite the rules: only the sender closes; with multiple senders, nobody closes directly (coordinate via WaitGroup); closing is signaling, not cleanup — unclosed channels are GC'd fine.
ex04 — nil channel in select `b 42`. Operations on nil channels block forever, so a nil case is simply never ready — select ignores it. If *all* cases were nil (and no `default`): deadlock. This is a load-bearing idiom: set a channel var to nil to disable its case dynamically (see exercise 07's README for it powering a fan-in).
ex05 — slice aliasing via append `[1 2 99 4]` and `[1 2 3 4]`. First: `a[:2]` has len 2, **cap 4** — append fits within cap, writes into the shared backing array, clobbers `a[2]`. Second: full slice expression `a[:2:2]` caps the cap at 2 — append must reallocate, `a` untouched. Corollaries: this is why `append` returns a value; why functions receiving slices can visibly mutate the caller's data *sometimes* (only when no realloc happened — the worst kind of bug); and what `s[low:high:max]` is actually for.
ex06 — defer + named returns `6` then `5`; then `deferred: 2, 1, 0` at main's exit, LIFO. In `f`, `return 5` assigns to the *named* return `x`, then defers run and mutate it → 6. In `g`, `return x` copies into an anonymous slot first → the defer's increment is invisible. Also hiding here: `defer fmt.Println("deferred:", i)` evaluates the *argument* `i` at defer time — so it's the deferred *closures* that capture, plain calls snapshot. Practical use of the f-pattern: wrapping errors on the way out via `defer func() { err = ... }()` with a named `err`.
ex07 — typed nil interface Prints `got error: boom` — the `err != nil` branch runs. An interface value is a (type, value) pair; here it's (`*MyErr`, nil) — a non-nil interface wrapping a nil pointer. Interface == nil only when BOTH halves are nil. (The method call then works because `Error()` never dereferences its nil receiver — nil receivers are legal in Go.) Fix: return literal `nil`, or declare `var err error` and assign. If you can whiteboard the two-word pair, you own this question.
ex08 — concurrent map writes Very likely dies with `fatal error: concurrent map writes` — and `recovered:` never prints, because it's a **runtime fatal error (throw), not a panic**: `recover()` cannot catch it, the process is gone. (Deliberate design: a corrupted map is unrecoverable state.) Fixes: mutex, `sync.Map` (append-mostly workloads only), or sharding. `-race` flags it deterministically even on runs that happen not to crash.
ex09 — WaitGroup Add placement Race: main can reach `wg.Wait()` while the counter is still 0 (no goroutine has run `Add` yet) → Wait returns instantly, prints `count: 0..3` — any value. Worse interleavings hit "WaitGroup misuse: Add called concurrently with Wait". Rule: `Add` in the *spawning* goroutine, before `go`. Plus `count++` is itself a data race (`-race` flags both). And always pass `*sync.WaitGroup` — copying one with nonzero state breaks the count (`go vet` catches the copy).
ex10 — goroutine leak on timeout Prints ~101 goroutines instead of 1. On timeout, nobody ever receives from the unbuffered `ch`; the worker goroutine blocks on `ch <- "body"` forever. At 100 rps against a slow upstream that's OOM with a countdown. The one-character-ish fix: `make(chan string, 1)` — the send always succeeds, the orphaned result gets GC'd with the channel. The structural fix: pass ctx into the work and select the send against `ctx.Done()`. Detection in prod: `go_goroutines` metric trending up + pprof goroutine profile showing an army parked in `chan send` at one line.
ex11 — method sets Compile error: `T does not implement Stringer (method String has pointer receiver)`. Method set of `T` = value-receiver methods only; method set of `*T` = both. Fix: `var s Stringer = &T{}`. The *why*: interface values can hold unaddressable copies (map elements, temporaries), so the compiler can't conjure a `*T` from a `T` for you. Related asymmetry people forget: calling `t.String()` directly on an addressable `t` works fine — the compiler inserts `(&t)` — it's only interface *satisfaction* that's strict.