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
66 lines
2.5 KiB
Markdown
66 lines
2.5 KiB
Markdown
# 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.
|