Worker Pools
You've been spawning one goroutine per item. Goroutines are cheap, but "cheap" isn't "free": a million-item slice becomes a million goroutines, each holding a stack and maybe a database connection or an open file. That's how you exhaust memory, sockets, or the patience of whatever you're calling. The fix is a worker pool: a fixed number of workers that share the work.
One channel, N workers
The shape is simple. Put every job on one channel. Start a small, constant
number of workers. Each worker loops over that same channel with range, and
Go hands each queued job to whichever worker is free:
jobs := make(chan int)
results := make(chan int)
// Start a fixed pool — say 3 workers.
for w := 0; w < 3; w++ {
go func() {
for n := range jobs { // all three share this one channel
results <- n * n
}
}()
}
No matter how many jobs you feed in, only three run at once. That's the whole
point: the pool size caps concurrency, independent of the queue length.
Feeding the jobs happens elsewhere, then you close(jobs) so every worker's
range ends.
Closing results with a WaitGroup
Here's the subtlety. Consumers range over results until it's closed — so
someone must close it, but only after the last worker is done. No single
worker knows it's last. A sync.WaitGroup answers exactly that question:
var wg sync.WaitGroup
for w := 0; w < 3; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := range jobs {
results <- n * n
}
}()
}
go func() {
wg.Wait() // block until all workers have returned
close(results) // now it's safe: nothing else will send
}()
The closer runs in its own goroutine so the main goroutine can keep receiving
from results while the workers are still producing. wg.Wait() is the "all
workers finished" signal; close(results) turns it into the "stream is over"
signal a range understands. This WaitGroup-then-close idiom is how you
signal completion on a channel with multiple senders.
Feeding jobs and reading results
Put the pieces together and you have a producer, a fixed pool, and a completion-driven close:
func main() {
jobs := make(chan int)
results := make(chan int)
var wg sync.WaitGroup
for w := 0; w < 3; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := range jobs {
results <- n * n
}
}()
}
go func() { // producer
for i := 1; i <= 9; i++ {
jobs <- i
}
close(jobs) // tells every worker's range to stop
}()
go func() { wg.Wait(); close(results) }()
for r := range results { // runs until results is closed
fmt.Println(r)
}
}
Pool vs. one-per-item
Both patterns fan work out; the difference is who bounds it. "One goroutine per item" scales concurrency with the input — great for a handful of items, a resource fire for a million. A worker pool fixes concurrency at a number you choose — often tuned to CPU cores, or to how many calls a downstream service tolerates. When the queue is large or each job touches a limited resource, reach for the pool.
One catch worth remembering: results arrive in whatever order workers finish, not the order you queued them. In the exercise you'll keep the output ordered anyway — by writing each result to its own index rather than relying on arrival order.
Practice
1. Square Pool
AdvancedImplement
squareAllso it squares every element of a slice using a fixed pool of exactlyworkerslong-lived goroutines:func squareAll(nums []int, workers int) []int- Create the result slice up front with
make([]int, len(nums)). - Start exactly
workersgoroutines. They allrangeover one shared jobs channel — that's what caps concurrency atworkers, no matter how longnumsis. - Send an index for each element onto the jobs channel, then
closeit so every worker'srangeends. Each worker writesnums[i] * nums[i]intoresult[i]— its own slot — so the output stays in input order regardless of which worker finishes first. - Wait for all workers with a
sync.WaitGroupbefore returning. - Order must match the input:
squareAll([]int{1, 2, 3, 4, 5}, 3)returns[]int{1, 4, 9, 16, 25}. - An empty or nil input returns an empty slice.
workerslarger thanlen(nums), andworkers == 1, must both still be correct.
The difference from "one goroutine per element" is that here the goroutine count is fixed — the pool drains a queue that can be far larger than itself.
package main import "fmt" // squareAll squares every element of nums using a pool of exactly `workers` // long-lived goroutines, returning the results in the SAME order as nums. func squareAll(nums []int, workers int) []int { // TODO: build a jobs channel carrying each index, start `workers` // goroutines that each `for j := range jobs { result[j] = nums[j]*nums[j] }`, // and use a sync.WaitGroup to wait for them all before returning. return nil } func main() { fmt.Println(squareAll([]int{1, 2, 3, 4, 5}, 3)) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Create the result slice up front with