# 07 — Fan-in (Merge) The mirror image of the worker pool: N producers, one stream. Every metrics pipeline, event aggregator, and multi-shard reader is this shape. ## The double-select most solutions miss A forwarder that's correct under cancellation needs `ctx.Done()` in **two** places: ```go for { v, ok := <-ch // (1) can block forever if input never closes if !ok { return } out <- v // (2) can block forever if consumer walked away } ``` Both blocking points must become selects. Miss (2) and you have the sneakier leak: the consumer stops reading (their own ctx died), your forwarders sit parked on `out <- v` until process exit. The test cancels *while* you're blocked on send, specifically to catch this. In pprof this leak has a signature: dozens of goroutines parked in `chan send` inside your merge function, ages in the minutes column. ## `runtime.NumGoroutine()` as a test assertion Counting goroutines before/after is a legit leak-detection technique — crude but effective. The polished version is `goleak` (from Uber), worth namedropping: it snapshots goroutines at test end and fails on strays. In production the equivalent is watching `go_goroutines` from the Prometheus client — a sawtooth that only goes up is a leak with a deadline. ## Nil-channel select: the elegant alternative For a *small, fixed* number of inputs there's a beautiful trick — select over all inputs directly and set a channel variable to `nil` when it closes: ```go for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil; continue } // case permanently disabled out <- v case v, ok := <-b: if !ok { b = nil; continue } out <- v } } ``` Ops on nil channels block forever, so a nil case simply never fires — it's the idiomatic way to *disable* a select arm at runtime. This version needs no WaitGroup and no forwarder goroutines. Know both: goroutine-per-input scales to dynamic N; nil-channel select wins for 2–3 known inputs. Explaining *why* the nil trick works (nil channel semantics) is exactly the kind of language-lawyer flex these interviews reward. ## Bonus rabbit hole: what select actually does With multiple ready cases, `select` picks **uniformly at random** — the runtime shuffles case order (`selectgo`) to prevent starvation. So merge output ordering isn't just "unspecified," it's actively randomized under contention. Determinism-dependent tests on select ordering are therefore always flaky — a code-review comment you'll get to make at least once a year for free.