WaitGroup
- A WaitGroup is a synchronization mechanism provided by theÂ
sync package. - It is used to wait for a collection of goroutines to complete their execution.
- TheÂ
WaitGroup allows you to add a count representing the number of goroutines that need to complete, and it provides methods to signal when a goroutine finishes and to block execution until all the goroutines have completed
Key Methods of sync.WaitGroup
Add(delta int): Increments the counter by the specifiedÂdelta value, typically the number of goroutines you want to wait for.Done(): Decrements the counter by 1. This is usually called at the end of a goroutine to indicate its completion.Wait(): Blocks the execution of the calling goroutine until the counter becomes zero.
Using WaitGroup
// This is a common idiom in Go.
wg := &sync.WaitGroup{} You often need to pass the WaitGroup to goroutines, and passing a pointer ensures all goroutines share the same instance.
| Declaration | Description |
|---|---|
var wg sync.WaitGroup | A value type. Can be used directly. |
wg := &sync.WaitGroup{} | A pointer type. Used when shared between functions/goroutines. |
wg := sync.WaitGroup | equivalent to var wg sync.WaitGroup |
func main() {
wg := &sync.WaitGroup{} // pointer
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("Worker:", id)
}(i)
}
wg.Wait()
}If you accidentally pass a copy of a WaitGroup to a function or goroutine: It won’t work as expected — you’ll wait forever, because you’re not updating the original.
func doSomething(wg sync.WaitGroup) {
wg.Add(1) // modifies a copy — doesn't affect the original
}Common Pitfalls
func main() {
wg := &sync.WaitGroup{}
// Intend to wait for one goroutine`
wg.Add(1)
go func() {
// defer wg.Done() // PROBLEM: Defer is missing!
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
fmt.Println("Error creating request:", err)
// We return early, wg.Done() is never called.
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
// We return early, wg.Done() is never called.
return
}
defer resp.Body.Close()
// Only call Done() on the happy path
fmt.Println("Response Status:", resp.Status) // <<< If errors happen, this line is skipped!
}()
wg.Wait() // <<< This will wait FOREVER if an error occurs above.
}Mutex
A Mutex is a mutual exclusion lock that prevents other processes from entering a critical section of data while a process occupies it to prevent race conditions from happening.
What's a critical section?
A critical section can be a piece of code that must not be run by multiple threads at once because the code contains shared resources.
When multiple goroutines access shared memory (like a variable, map, or struct) concurrently, it can lead to race conditions — unpredictable, buggy behavior.
A mutex serializes access to such shared data, allowing only one goroutine to access the critical section at a time.
package main
import (
"fmt"
"sync"
)
func main() {
wg := &sync.WaitGroup{} // pointer
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("Worker:", id)
}(i)
}
wg.Wait()
}func doSomething(wg sync.WaitGroup) { // Value type
wg.Add(1) // modifies a copy — doesn't affect the original
}Protecting shared map
The sync.Mutex type, like many types in Go, is safe to use as a zero value. we don’t want to explicitly initialize it.
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
func main() {
sm := SafeMap{m: make(map[string]int)}
// sm := &SafeMap{m: make(map[string]int)} <<< If we want pass the map to modify.
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[fmt.Sprintf("worker-%d", id)] = id
}(i)
}
wg.Wait()
fmt.Println(sm.m)
}// Use unlock with defer
sm.mu.Lock()
defer sm.mu.Unlock()func unsafeCopy(sm SafeMap) {
sm.mu.Lock() // <- this is a copy of the original mutex
defer sm.mu.Unlock()
// does not actually lock the original SafeMap!
}
----
func safeAccess(sm *SafeMap) {
sm.mu.Lock()
defer sm.mu.Unlock()
// safely access sm.m
}