What are advanced concurrency patterns in Go?

Concurrency is one of Go’s defining features that provides a set of powerful built-in primitives like goroutines and channels that make concurrent programming relatively simple. However, as Go programs grow in complexity, more advanced concurrency patterns become essential. In this Answer, we’ll explore advanced concurrency patterns in Go that go beyond the basics and help us tackle complex concurrent tasks with elegance.

Why advanced concurrency patterns?

While goroutines and channels can handle many concurrent scenarios, advanced concurrency patterns are required when we need to address specific challenges, such as fine-grained control over goroutine lifecycles, synchronized access to shared resources, or handling complex workflows.

Let’s discuss common advanced concurrency patterns:

The worker pool pattern

The worker pool pattern is used to control the number of concurrent tasks running at a given time. It involves creating a pool of goroutines ready to perform tasks. Tasks are distributed to idle workers to ensure a fixed number of tasks are executed simultaneously. This pattern is beneficial in scenarios like web scraping or managing concurrent I/O-bound operations.

Example

package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
const numWorkers = 3
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
worker(w, jobs, results)
}(w)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Printf("Result: %d\n", r)
}
}

Explanation

  • Line 8: We create a worker function that represents a worker goroutine. It takes an id, a jobs channel to receive tasks, and a results channel to send results.
  • Lines 8–13: Inside the worker function, it continuously receives jobs from the jobs channel, processes them, and sends the results back to the results channel.
  • Lines 15–44: In the main function, we set the number of jobs and workers. We create two channels, jobs and results, to manage tasks and results. We use WaitGroup from the sync package to wait for all workers to finish their tasks.

This pattern is useful when we need to control the number of concurrent tasks, like when performing I/O-bound operations concurrently.

The select statement with default

The select statement in Go allows us to wait on multiple communication operations. Adding a default clause to a select statement enables non-blocking communication. This is incredibly useful in scenarios where we want to perform an action if a channel is ready but not block if it’s not.

Example

package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
c1 <- "Two seconds"
}()
select {
case msg1 := <-c1:
fmt.Println("Received", msg1)
case <-time.After(1 * time.Second):
fmt.Println("Timed out on c1")
}
select {
case c2 <- "Hello":
fmt.Println("Sent 'Hello' to c2")
default:
fmt.Println("c2 is not ready for data.")
}
}

Explanation

  • Lines 8–22: We create two channels, c1 and c2, to simulate communication channels. We start a goroutine that sends a message to c1 after a delay. We use a select statement to wait for communication from either c1 or a timeout from time.After(1 * time.Second).

  • Line 23–30: We use a select statement to try sending “Hello” to c2. If c2 is ready to receive data, we send the message. Otherwise, we take the default branch, indicating that c2 is not ready.

This pattern is valuable when handling multiple channels’ communication and performing non-blocking operations. The default clause allows us to execute code if none of the other communication operations is ready.

Conclusion

Advanced concurrency patterns in Go extend the language’s already impressive concurrency features. By mastering these patterns, we can write more efficient and reliable concurrent programs, making Go an even more compelling choice for developing concurrent and parallel software. Understanding when and how to use these patterns can empower us to build highly concurrent applications that efficiently utilize modern multi-core processors and distributed systems.

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved