Why start a fixed number of worker goroutines reading one jobs channel instead of one goroutine per job?
answer
- who decides how many run at once
- goroutines are cheap; what they hold is not
- input size must not set concurrency
- N loops over one channel, fixed at start
- one close retires the whole fan-out
basics
~20 sA fixed pool caps how much work runs at once, so memory, open sockets and downstream load stay bounded however many jobs arrive. One goroutine per job lets a million jobs become a million concurrent callers.
solid answer
~40 sGoroutines themselves are cheap, so the reason to bound them is what each one *holds* while it runs: a database connection, an outbound request, a decoded payload. A worker pool fixes that at construction — start N goroutines, each looping `for j := range jobs`, and every job is taken by whichever worker is free. Work in flight is N whether you feed it ten jobs or ten million; the channel is the queue, so the backlog can be any length. One goroutine per job instead makes concurrency a function of input size, which is the one variable you do not control: an unusually large batch turns into thousands of simultaneous connections. The pool also gives you a single shutdown switch — close the jobs channel and all N workers return.
code
go · 29 linesfunc run(inputs []int, workers int) []int {
jobs := make(chan int)
results := make(chan int, len(inputs))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs { // N receivers, one shared channel
results <- j * 2
}
}()
}
for _, in := range inputs {
jobs <- in
}
close(jobs)
wg.Wait()
close(results)
out := make([]int, 0, len(inputs))
for r := range results {
out = append(out, r)
}
return out
}go deeper
Be ready to describe the shape from memory: a jobs channel, N goroutines each looping over it, and a close at the end. Say plainly that the pool limits how much work happens at the same time.
Explain that the bound protects resources the goroutines hold - connections, buffers, descriptors - not the goroutines themselves, and that the channel doubles as the queue that slows a fast producer down.
An interviewer expects you to connect the bound to a real incident: an oversized batch turning into a connection storm, and the pool as the thing that makes peak load independent of input size.
Be able to argue where the bound belongs - in the library, in the caller, or in the platform - and what you give up by hard-coding a worker count inside a package other teams import.
## The two decisions a pool makes A worker pool is two decisions taken once, at construction: **how many goroutines will exist**, and **which channel they all read from**. Everything else follows. ```go jobs := make(chan job) results := make(chan result) var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { // one shared channel, N receivers results <- do(j) } }() } ``` `workers` is a number you chose. It does not depend on how many jobs there are, and that independence is the entire point. ## Why not one goroutine per job The usual first answer — "goroutines are expensive" — is wrong, and an interviewer will push on it. A goroutine starts with a small growable stack (a couple of kilobytes) and the runtime happily schedules hundreds of thousands of them. If the only cost were the goroutine, `go handle(j)` in a loop would be fine. The cost is **what each goroutine holds while it runs**: - a connection from a database pool, or an outbound socket and its TLS session; - a decoded request body, a `[]byte` read from a file, a JSON tree — live memory the collector cannot reclaim while the goroutine is running; - a file descriptor, and the process has a hard limit on those; - a slot of attention from whatever service it is calling. One goroutine per job makes the peak of all of those a function of **input size**. Input size is exactly the variable you do not control: a batch that is normally 200 rows arrives one morning with two million, and the program tries to open two million connections. The failure does not look like a Go problem — it looks like the database refusing connections, or the downstream API returning 429s and then 503s, or the process dying on file-descriptor exhaustion. A pool inverts that. Work *in flight* is `workers`, forever. Work *waiting* is whatever is sitting in the jobs channel or blocked at the producer's send. The queue can be any length; the concurrency is a constant. ## The channel is the queue There is no separate queue object. `jobs` is the queue: a send puts work in, a receive takes work out, and when all N workers are busy the producer's send blocks until one of them comes back for more. That blocking is a feature — it is how a fast producer is slowed to the speed of the pool instead of building an unbounded backlog in memory. Because every worker runs the same `for j := range jobs` loop, dispatch needs no logic at all. A worker that finishes early simply loops and takes the next value; a worker stuck on a slow job takes nothing. Load balances itself, without a scheduler you wrote. ## One shutdown switch The other reason to fix the count at construction is that it gives you exactly one thing to close. Closing `jobs` ends every worker's `range` loop after the channel drains, each worker returns, and the `WaitGroup` falls to zero. You do not have to track a set of goroutines, cancel them individually, or hope they notice a flag. N goroutines were started in one place and they end in one place. With one goroutine per job there is no such switch: the set of live goroutines is whatever the input happened to create, and stopping them means threading cancellation into every one. ## When one goroutine per job is fine The rule is not "never spawn per item" — it is **never let input size set concurrency**. Fanning out to three replicas, or launching one goroutine per configured shard, is bounded by something you control, so per-item is fine there. The moment the count comes from a file, a request payload, or a database result set, it needs a bound. ## What interviewers listen for - Naming a *resource* the pool protects, not just "memory" in the abstract. - Understanding that the pool bounds concurrency, not total work — 10 million jobs still all run, just `workers` at a time. - Knowing the worker count is chosen at construction and not derived from the job count. - Not confusing the pool with `GOMAXPROCS`: `GOMAXPROCS` bounds how many goroutines execute Go code simultaneously; it does not stop you from creating a million goroutines, and it does not bound the connections those goroutines open.
- Goroutines cost only a couple of kilobytes each, so what actually breaks when you launch one per job?The resources each one holds. A million live goroutines means a million in-flight database connections or sockets, a million file descriptors, and every decoded payload resident at once. The Go scheduler copes; the database, the file-descriptor limit and the heap do not. The downstream service sees your input size as a load spike.
- How do the workers in a pool know when to stop?The producer closes the jobs channel after its last send. Each worker's `for j := range jobs` drains what is left and then ends, the worker returns and runs its deferred `wg.Done()`, and `wg.Wait()` returns once all N have. One close retires every worker; you never track them individually.
- Is starting a goroutine per unit of work ever the right call?Yes, when the count is bounded by something you control rather than by input: fanning a request out to three replicas, or one goroutine per configured shard. The rule is not "never spawn per item", it is "never let input size set concurrency". The moment the count comes from a file or a result set, it needs a bound.
It is a checkout hall with a fixed number of tills and one queue. The queue can be any length, but only N shoppers are ever being served at once.
saying these in an interview costs you the question
- Says goroutines are free, so spawn one per job
- Thinks the pool exists only to save goroutine stack memory
- Sizes the pool from the number of jobs
- Starts another goroutine inside the worker for each job
- Confuses GOMAXPROCS with a limit on goroutine creation