Introduction
Unbounded goroutine creation can overwhelm downstream resources like databases and APIs. Worker pools limit concurrency by processing jobs through a fixed number of goroutines, providing natural backpressure.
Key Concepts
- Worker Pool: A fixed number of goroutines that read from a shared jobs channel and write to a results channel.
- Backpressure: When all workers are busy, new job senders block until a worker is free, naturally throttling throughput.
- Bounded Concurrency: Unlike spawning a goroutine per task, worker pools cap the number of concurrent operations.
Real World Context
A thumbnail generation service that needs to process uploaded images should not spawn 10,000 goroutines for 10,000 uploads — that would exhaust memory and overwhelm the disk. A worker pool of, say, 20 workers processes images in batches, keeping resource usage predictable.
Deep Dive
To limit concurrency (e.g., only 5 database connections at once), use a Worker Pool:
Pattern
- Create a
jobschannel and aresultschannel. - Spawn
Nworker goroutines that loop over thejobschannel. - Send tasks to
jobs. - Close
jobsto signal no more work.
gofunc worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "processing", j) results <- j * 2 } }
Benefits
- Limits concurrent operations to N.
- Reuses goroutines instead of creating new ones per task.
- Backpressure: if workers are busy, job senders block.
Common Pitfalls
- Not closing the jobs channel — If you forget to close the jobs channel, all workers block forever on
range jobsafter the last job, causing a goroutine leak. - Reading results before all jobs are sent — If the results channel is unbuffered and you try to read results before sending all jobs, you can deadlock. Either buffer the results channel or use a separate goroutine to collect results.
Best Practices
- Size the pool based on the bottleneck — If workers hit a database, size the pool to match the connection pool. If workers are CPU-bound, size to
runtime.NumCPU(). - Use errgroup for error-aware worker pools —
errgroup.GroupwithSetLimit()provides a cleaner API with automatic error handling.
Summary
- Worker pools limit concurrency via a fixed number of goroutines reading from a jobs channel.
- Close the jobs channel to signal completion; workers exit their range loop.
- Backpressure naturally throttles senders when all workers are busy.
- Consider
errgroup.SetLimit()as a higher-level alternative.
Code Examples
go
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 5; a++ {
<-results
}
}