skip to content

If three worker goroutines each run `for j := range jobs` on the same channel, how many of them see each job?

level: middleimportance: should knowfreq 60%

answer

  1. a queue, not a broadcast
  2. one value, one receiver
  3. which worker wins is unspecified
  4. results arrive in completion order
  5. one close ends all three loops

basics

~20 s

Exactly one. A channel receive is a hand-off, not a broadcast: each value goes to a single receiver, so three workers ranging over one channel split the jobs between them rather than each processing all of them.

solid answer

~50 s

Each value sent is received exactly once, by one goroutine — that is what makes fan-out free in Go, since you start N workers on one channel and need no dispatcher. Which worker gets a given job is unspecified, so never assume round-robin or an even split; in practice a worker busy with a slow job simply is not at the receive, so the work redistributes itself. Because the workers finish at different times, results come back in completion order, not submission order — if you need order, carry the index in the job and put it back in the result, or have each worker write into its own slot of a preallocated slice. No mutex is needed around the channel: channel operations are safe for concurrent use by multiple goroutines. When the sender closes the channel, every ranging worker drains what is buffered and then all three loops end.

code

go · 20 lines
go
inputs := []int{3, 1, 4, 1, 5}
out := make([]int, len(inputs)) // one slot per job, allocated up front
jobs := make(chan int)

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for idx := range jobs { // exactly one worker gets each idx
			out[idx] = inputs[idx] * 2
		}
	}()
}

for i := range inputs {
	jobs <- i
}
close(jobs)
wg.Wait() // out is fully written, in input order

go deeper

for a junior

Know the one-line fact: a value sent on a channel is received by exactly one goroutine. Three workers on one channel share the work; they do not each get a copy.

for a middle

Explain that which worker wins is unspecified, why that makes the pool self-balancing when jobs differ in cost, and why results therefore arrive out of order.

for a senior

Show how you restore ordering when a caller needs it - an index carried through the job, or per-index slice writes - and be able to say why neither needs a mutex.

for a principal

Be ready to decide whether your pool's public contract promises ordering at all, and what that promise costs in buffering and latency for every team that consumes it.

## A channel is a queue, not a broadcast The single most useful sentence about channels in a worker pool: **each value sent is received exactly once, by exactly one receiver.** Three goroutines all running ```go for j := range jobs { // ... } ``` on the same `jobs` channel are three consumers of one queue. Send nine values and nine units of work happen, split across the three in some proportion — not twenty-seven. This is what makes fan-out free in Go. You do not write a dispatcher, you do not shard the input, you do not hand each worker its own channel. You start N goroutines on one channel and the runtime does the handing-out. ## Which worker gets which job is not specified The language says nothing about *which* ready receiver a value goes to. In practice the runtime keeps a queue of goroutines parked on a receive and hands the value to one of them, but that is an implementation detail, not a guarantee, and it interacts with which workers happen to be busy. Do not build anything on the identity of the worker: no "worker 0 gets the even jobs", no assumption of round-robin, no per-worker state that assumes an even split. If a job must go to a particular worker, it needs its own channel — that is a different design, not a property you can extract from a shared one. The practical consequence is that a slow job does not starve the others: a worker stuck on one item simply is not at the receive, so its share of the remaining work goes to zero automatically. That self-balancing is the reason a shared channel beats pre-splitting the input into N slices, where one unlucky slice full of slow items leaves the other workers idle. ## Results come back out of order Because the workers run concurrently and finish at different times, results arrive on the results channel in completion order, which has no relationship to submission order. If order matters you have two clean options: - **Carry the index.** Put the input position in the job struct, put it back in the result struct, and sort or index by it once collection is done. - **Write into a preallocated slice.** Give the workers the index, and let each write `out[i]`. Distinct indices are distinct memory, so there is no data race and no mutex needed — but the slice must be allocated to its final length up front and never `append`ed to by a worker, because `append` may reallocate and two workers appending is a race. ## No mutex around the channel Channel operations are safe for concurrent use by multiple goroutines — that is a property of the channel itself, not something you add. Wrapping a receive in a `sync.Mutex` is a red flag in review: it does nothing for correctness and serialises the very hand-off you are trying to parallelise. The same applies to the send side: many producers may send on one channel concurrently. (Only the *close* has an ownership rule, because a close concurrent with a send is a panic, not a race the mutex would fix.) ## What close does to N ranging workers Closing the jobs channel is the pool's shutdown switch precisely because of the one-value-one- receiver rule turning into an all-receivers rule at close: - Values already buffered are **not** discarded. Every worker's `range` keeps yielding until the buffer is empty. - Once it is empty, every ranging loop ends — all N of them, not just one. A closed channel is permanently ready to receive, returning the zero value with `ok == false`, so each blocked receiver wakes and each `range` terminates. - A worker in the middle of processing a job is untouched; it finishes that job, comes back to the receive, sees the closed channel, and returns. So `close(jobs)` retires the whole fan-out with one statement, which is exactly why the pattern scales down cleanly. ## The misconceptions this catches Candidates who think `range` over a channel broadcasts are usually importing intuition from publish/subscribe systems or from event emitters in other languages. Go has no built-in broadcast over a channel of values; the closest thing is `close(done)` on a signalling channel, where every receiver observes the close — and note that what they observe is the *close*, not a value. Values never fan out to multiple receivers. The other common error is expecting the results to line up with the inputs. In a pool they never do unless you make them, and "make them" means carrying an index, not adding sleeps or reducing the worker count to one.

  • Do the results come back in the order the jobs went in?
    No. Workers run concurrently and finish at different times, so results arrive in completion order. If order matters, carry the input index in the job and return it in the result so you can sort or index by it, or give each worker the index and let it write `out[i]` in a slice preallocated to its final length — distinct indices are distinct memory, so no lock is needed.
  • Do the workers need a mutex around the receive since they share the channel?
    No. Channel operations are safe for concurrent use by multiple goroutines; that is a property of the channel. Adding a mutex is a review red flag: it buys no correctness and serialises the hand-off you are parallelising. Only closing has an ownership rule, and a mutex would not fix a close racing a send anyway.
  • If the jobs channel is buffered and still holds fifty values when the producer closes it, do the workers stop immediately?
    No. Closing is not cancelling. Each worker's range keeps yielding buffered values until the channel is empty, and only then does the loop end. A worker already processing a job finishes it, comes back to the receive, sees the closed and drained channel, and returns.

One queue, three tellers. Each customer is served by exactly one teller, and who serves whom depends on who is free.

saying these in an interview costs you the question

  • Thinks each worker receives a copy of every job
  • Wraps the channel receive in a mutex
  • Expects results in submission order
  • Assumes the runtime hands jobs out round-robin
  • Believes closing a channel discards its buffered values