Skip to Content
Course content

32: Sync Package: WaitGroups and Mutexes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a snippet of code that looks perfectly fine at a glance. It's a common pattern: you have a list of tasks, you fire off a goroutine for each one to speed things up, and you collect the results in a map. But if you run this, Go will likely scream at you with a fatal error.

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	urls := []string{"google.com", "github.com", "golang.org", "stackoverflow.com"}
	results := make(map[string]string)
	var wg sync.WaitGroup

	for _, url := range urls {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			// Simulate a network request
			time.Sleep(time.Millisecond * 10) 
			results[u] = "OK" 
		}(url)
	}

	wg.Wait()
	fmt.Println("Finished:", results)
}

The Panic: Concurrent Map Writes

If you run the code above, you'll probably see fatal error: concurrent map writes. This is one of the most important lessons in Go concurrency: Maps are not thread-safe.

While the sync.WaitGroup is doing its job correctly—making sure the main function doesn't exit before the goroutines finish—it does nothing to protect the data inside those goroutines. When two or more goroutines try to write to the results map at the exact same time, Go panics to prevent memory corruption. It's a hard crash because there's no safe way to recover once the map's internal structure is compromised.

Protecting State with Mutexes

To fix this, we need a way to ensure that only one goroutine can touch the map at a time. This is where sync.Mutex (short for "mutual exclusion") comes in. Think of a Mutex as a physical key to a room. If you want to enter the room (access the map), you have to take the key. If someone else already has it, you have to wait in line until they put it back.

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	urls := []string{"google.com", "github.com", "golang.org", "stackoverflow.com"}
	results := make(map[string]string)
	
	var wg sync.WaitGroup
	var mu sync.Mutex // This is our "key"

	for _, url := range urls {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			time.Sleep(time.Millisecond * 10)
			
			mu.Lock()         // Grab the key
			results[u] = "OK" // Safe to write now
			mu.Unlock()       // Give the key back
		}(url)
	}

	wg.Wait()
	fmt.Println("Finished:", results)
}

By wrapping the map assignment in mu.Lock() and mu.Unlock(), we've created a "critical section." Only one goroutine can execute the code between the lock and unlock at any given moment. I usually recommend keeping the locked section as small as possible. Notice I didn't put the time.Sleep inside the lock—that would defeat the whole purpose of using goroutines because everything would happen sequentially.

Coordinating the Finish Line with WaitGroups

You've already seen sync.WaitGroup in action here, but let's be explicit about why we use it. Without wg.Wait(), the main function would hit the end of its block and terminate the process immediately. It wouldn't care that there are four goroutines still sleeping or writing to a map; it would just kill them instantly.

Here are the three golden rules I follow when using WaitGroups:

  • Add before you go: Always call wg.Add(1) in the parent goroutine, before the go keyword. If you put wg.Add(1) inside the goroutine itself, there's a race condition where wg.Wait() might be reached before the goroutine even starts, causing the program to exit prematurely.
  • Defer the Done: Use defer wg.Done() at the very top of your goroutine. This ensures that even if your function panics or returns early via an error check, the counter is still decremented. Otherwise, your program will deadlock forever.
  • Pass by Pointer: If you pass a WaitGroup into another function, you must pass it as a pointer (*sync.WaitGroup). If you pass it by value, the function gets a copy of the WaitGroup, and calling Done() on that copy won't affect the original counter in main.



📋 Practical Task

Exercise: Build a Concurrent Portfolio Value Aggregator

You are building a tool that fetches the current price of several cryptocurrencies from a mock API and calculates the total value of a user's portfolio.

Requirements:

  • Create a portfolio map where the key is the coin symbol (e.g., "BTC", "ETH") and the value is the amount owned (e.g., 0.5, 10.0).
  • Create a prices map to store the fetched price for each coin.
  • Write a function fetchPrice(coin string) float64 that simulates a network delay using time.Sleep and returns a random float.
  • Use a sync.WaitGroup to fetch prices for all coins in the portfolio concurrently.
  • Use a sync.Mutex to safely write the fetched prices into the prices map.
  • Once all fetches are complete, calculate the total portfolio value (sum of amount * price for all coins) and print it to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.