Files
learning-go-for-sre-interview/07-fan-in
kacerrandClaude Fable 5 7219eb5a23 Initial commit: Go SRE interview exercises
Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
2026-08-28 22:18:09 +02:00
..

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:

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:

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.