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
2.1 KiB
02 — Worker Pool
The fundamental fan-out/fan-in shape. Everything else (pipelines, errgroup, batch processors) is a decoration of this.
The channel-closing protocol
The whole exercise is really about one question: who closes what, and when?
Rules worth engraving:
- Only the sender closes. A receiver closing a channel = eventual panic on
the sender side (
send on closed channel). - Close is a broadcast. Every
rangeand everyv, ok := <-chacross all receivers unblocks. That's whyclose(jobs)cleanly retires all workers. - With N senders, no single sender may close. That's the "closer goroutine" pattern you need here:
go func() {
wg.Wait()
close(out)
}()
All N workers send to out; a dedicated goroutine waits for all of them and
then closes. This is the only race-free way to close a multi-producer channel.
- You don't have to close channels at all. Channels are GC'd like anything
else. Closing is signaling, not resource cleanup. You close
outhere only because the consumer ranges over it.
The subtle leak in the naive version
If you forget close(out), the consumer's range blocks forever — the test
catches this with a timeout. In prod this presents as goroutine count creeping
up one per request, which you'd see in go_goroutines on your dashboard long
before OOM. Well... ideally you would.
Why unbuffered out is fine (and interesting)
With out unbuffered, workers block until the consumer reads → natural
backpressure, memory bounded by workers. Buffering out decouples throughput
spikes but the buffer is a queue, and queues hide latency — a very SRE
trade-off to mention out loud. (Little's law applies to your channels too.)
Interview follow-ups to prep
- "Add context cancellation" → every
out <- fn(j)becomes aselectwithctx.Done(), and job consumption too. - "A job can fail" → either a Result{V, Err} envelope type, or switch to
errgroupand talk about fail-fast vs collect-all-errors semantics. - "Jobs arrive faster than they're processed" → bounded jobs channel = backpressure at the producer; discuss shedding vs queueing.