Concurrency Patterns
Once you’re comfortable with goroutines and channels, a handful of composable patterns cover almost every real concurrency problem.
Worker pool
Section titled “Worker pool”Bound the number of concurrent workers regardless of how many jobs arrive:
func workerPool(jobs <-chan int, results chan<- int, workerCount int) { var wg sync.WaitGroup for i := 0; i < workerCount; i++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { results <- j * j } }() } wg.Wait() close(results)}Pipelines
Section titled “Pipelines”Chain stages together, each reading from one channel and writing to the next:
func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out}
func square(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out}
// usage: for n := range square(generate(1, 2, 3)) { ... }Each stage is independently testable and the whole pipeline runs concurrently — stage two starts squaring the first value while stage one is still generating the rest.
Fan-in
Section titled “Fan-in”Merge several channels into one:
func fanIn(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup wg.Add(len(channels))
for _, c := range channels { go func(c <-chan int) { defer wg.Done() for v := range c { out <- v } }(c) }
go func() { wg.Wait() close(out) }() return out}Avoiding leaks and races
Section titled “Avoiding leaks and races”- Always have a plan for how a channel gets
closed — a goroutine blocked forever on a channel that’s never closed or read from is a leak. - Run tests and real workloads with the race detector:
go test -race ./...orgo run -race main.go. - Prefer passing a
context.Contextfor cancellation over ad-hoc “done” channels once you have more than one exit condition (covered next chapter).