Working with WaitGroups
In the previous lab, we used time.Sleep to wait for goroutines to complete, but this approach is unreliable. A better way to synchronize goroutines is to use WaitGroups from the sync package.
What is a WaitGroup?#
A WaitGroup is a synchronization primitive that allows you to wait for a collection of goroutines to finish executing. It provides three main methods:
Add(delta int): Adds delta (a positive or negative value) to the WaitGroup counterDone(): Decrements the WaitGroup counter by 1Wait(): Blocks until the WaitGroup counter is 0
Basic Pattern#
The basic pattern for using WaitGroups is:
- Create a WaitGroup
- Call
Addto set the number of goroutines to wait for - Launch goroutines and have each call
Donewhen finished - Call
Waitto block until all goroutines are done
Simple Example#
Here's a simple example of using WaitGroups:
package mainimport (string">"fmt"string">"sync")func printSomething(s string, wg *sync.WaitGroup) {string">"comment">// Ensure we call Done when this function returnsdefer wg.Done()fmt.Println(s)}func main() {string">"comment">// Create a WaitGroupvar wg sync.WaitGroupstring">"comment">// Add the number of goroutines we'll wait forwg.Add(3)string">"comment">// Launch 3 goroutinesgo printSomething(string">"First message", &wg)go printSomething(string">"Second message", &wg)go printSomething(string">"Third message", &wg)string">"comment">// Wait for all goroutines to finishwg.Wait()fmt.Println(string">"All goroutines completed")}
In this example:
- We create a WaitGroup
wg - We call
wg.Add(3)to indicate we'll wait for 3 goroutines - We launch three goroutines, each calling
wg.Done()when finished - We call
wg.Wait()to block until all goroutines are done
Processing a Collection with WaitGroups#
Here's a more practical example that processes items from a slice:
package mainimport (string">"fmt"string">"sync")func processItem(i int, item string, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf(string">"%d: %s\n", i, item)}func main() {var wg sync.WaitGroupwords := []string{string">"one",string">"two",string">"five",}string">"comment">// Add the number of items we need to processwg.Add(len(words))string">"comment">// Launch a goroutine for each itemfor i, word := range words {go processItem(i, word, &wg)}string">"comment">// Wait for all processing to completewg.Wait()fmt.Println(string">"All items processed")}
Important Considerations#
1. Always Pass WaitGroups by Pointer#
WaitGroups must be passed by pointer. If you pass them by value, each goroutine gets a copy, and calling Done() on those copies won't affect the original WaitGroup.
string">"comment">// Correct: passing by pointergo someFunction(&wg)string">"comment">// Incorrect: passing by valuego someFunction(wg) string">"comment">// This won't work as expected
2. Call Add Before Launching Goroutines#
Always call Add before launching the goroutines to avoid race conditions where Wait might return before all goroutines have started.
string">"comment">// Correctwg.Add(3)go func1(&wg)go func2(&wg)go func3(&wg)string">"comment">// Incorrect - race condition possiblego func1(&wg) string">"comment">// wg.Add(1) might not happen before Wait() checks countergo func2(&wg)go func3(&wg)
3. Use defer wg.Done()#
It's good practice to use defer wg.Done() at the beginning of your goroutine functions to ensure Done() is called even if the function panics.
func worker(wg *sync.WaitGroup) {defer wg.Done() string">"comment">// This will be called even if the function panicsstring">"comment">// Do work...}
4. Balance Add and Done#
Make sure the number of Done() calls matches the number of increments from Add(). If you call Done() too few times, Wait() will block forever. If you call it too many times, you'll get a panic.
Advanced Example: WaitGroup with Message Passing#
Here's a more advanced example that combines WaitGroups with channels for message passing:
package mainimport (string">"fmt"string">"sync")func updateMessage(s string, wg *sync.WaitGroup, m *string) {defer wg.Done()*m = s}func printMessage(m *string) {fmt.Println(*m)}func main() {var wg sync.WaitGroupmsg := string">"Hello, world!"wg.Add(1)go updateMessage(string">"Hello, One!", &wg, &msg)wg.Wait()printMessage(&msg)wg.Add(1)go updateMessage(string">"Hello, two!", &wg, &msg)wg.Wait()printMessage(&msg)wg.Add(1)go updateMessage(string">"Hello, three!", &wg, &msg)wg.Wait()printMessage(&msg)}
In this example, we're using WaitGroups to ensure that each message update is completed before printing it.
Summary#
- WaitGroups provide a reliable way to wait for goroutines to complete
- The basic pattern is: Create, Add, Launch goroutines with Done, Wait
- Always pass WaitGroups by pointer
- Call Add before launching goroutines
- Use defer wg.Done() for safety
- Ensure Add and Done calls are balanced
WaitGroups are a fundamental synchronization mechanism in Go, and they're often combined with other concurrency patterns to build robust concurrent programs.
In the next lab, we'll learn how to write tests for concurrent code using WaitGroups.