Concurrency Patterns in Go: Wait for Results
In the previous article, "Concurrency Patterns in Go: A Practical Guide," I took a fun and practical dive into the world of concurrency in Go. I explored how goroutines, channels, and synchronization primitives like sync.Mutex
and sync.WaitGroup
make it easy to write concurrent programs. I also used a relatable real-life example—exam invigilation—to explain how concurrency works in practice. If you haven’t read it, I highly recommend checking it out here to get up to speed.
In this article, I will take the next step and explore two essential concurrency patterns: Wait for Results and Fan-Out. These patterns are the bread and butter of building scalable and efficient systems in Go. To make things easy to understand, we’ll continue using the exam invigilation scenario and extend it with a practical API endpoint example. By this article’s end, you’ll understand these patterns and feel confident applying them to your projects.
Wait for Results with sync.WaitGroup
What Is the Wait for Results Pattern?
Imagine you’re a teacher supervising an exam. You have 10 students writing their answers, and you need to wait for all of them to finish before collecting their papers. This is precisely what the Wait for Results pattern does in programming. It ensures that your program waits for multiple tasks (goroutines) to complete before moving on to the next step.
Go achieves this using the sync.WaitGroup
type. Think of it as a counter that tracks how many tasks are still running. All tasks are done when the counter reaches zero, and your program can proceed.
Real-Life Scenario: Exam Invigilation
Let’s revisit the exam invigilation scenario. You’re the invigilator, and your job is to:
Start the exam for all students.
Wait for all students to finish.
Collect their answer sheets.
This is a perfect example of the wait-for-results pattern. Each student is like a goroutine, and you (the invigilator) are the main
program waiting for all of them to finish.
How It Works in Go
Here’s how you can model this scenario in Go using sync.WaitGroup
. Instead of just showing the code, I encourage you to try it in the Go Playground. Click the link below to run the code and see the results in real-time:
Go Playground: Wait for Results Example
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
rand.Seed(time.Now().UnixNano())
// simulate 10 students taking the exam
for i := 0; i < 10; i++ {
// increment the WaitGroup counter
wg.Add(1)
go monitorStudents(i, &wg)
}
fmt.Println("Invigilator: Exams started. Waiting for students to finish...")
// block the main goroutine until all students finish
wg.Wait()
fmt.Println("Invigilator: All papers collected!")
}
func monitorStudents(studentID int, wg *sync.WaitGroup){
// decrement the counter when done
// the `defer` keyword here executes in last in, first out (LIFO) order,
// allowing for precise control over cleanup task
defer wg.Done()
fmt.Printf("Student %d started the exam\n", studentID)
// simulating a random exam time (1-5 secs)
time.Sleep(time.Duration(rand.Intn(5)+1) * time.Second)
fmt.Printf("Student %d finished\n", studentID)
}
What’s Happening Here?
wg.Add(1)
: Before starting each student (goroutine), we increment theWaitGroup
counter by 1.wg.Done()
: When a student finishes, we decrease the counter by callingwg.Done()
.wg.Wait()
: The invigilator (primary program) waits until the counter reaches zero, meaning all students are done.
Key Takeaways
Use
sync.WaitGroup
to finish multiple goroutines.Think of it as a counter that tracks active tasks.
This pattern is perfect for scenarios like batch processing or waiting for various API calls to complete.
Conclusion
The Wait for Results pattern is essential for synchronizing concurrent tasks in Go. It ensures that all goroutines are completed before proceeding. By using mechanisms like WaitGroups
or channels
, you can efficiently collect results and maintain control over execution flow.
This pattern is handy when handling tasks that require aggregation, such as processing API responses, analyzing large datasets, or waiting for computations to finish. Properly implementing Wait for Results prevents premature termination and ensures data consistency.
Next time, you must coordinate multiple goroutines, so rely on Wait for Results to keep your system efficient and synchronized.
PS: If you enjoyed this article, stay tuned for the next part of the series, where I will explore each concurrency pattern in more detail with practical examples.