Go WaitGroups — Synchronizing Goroutines with sync.WaitGroup
In this tutorial, you will learn about Go WaitGroups. We cover key concepts, practical examples, and best practices to help you master this topic.
Go sync.WaitGroup synchronizes Goroutine completion with Add, Done, and Wait methods for coordinating parallel work.
What You'll Learn
- WaitGroup fundamentals (Add/Done/Wait)
- Pattern for launching goroutines
- WaitGroup with error handling
- Common patterns and pitfalls
Why It Matters
WaitGroups ensure all goroutines complete before the program exits. Docker uses WaitGroups for build stages. Kubernetes uses them for parallel task execution. DodaZIP uses WaitGroups for parallel file processing.
Real-World Use
Parallel data processing, batch job execution, concurrent API calls, test suite coordination, build pipeline stages.
flowchart LR
A["WaitGroup"] --> B["Add"]
B --> C["Done"]
C --> D["Wait"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic WaitGroup
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id)
}(i)
}
wg.Wait()
fmt.Println("All workers completed")
}
WaitGroup with Results
func main() {
var wg sync.WaitGroup
results := make(chan int, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
results <- n * n
}(i)
}
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println("Result:", r)
}
}
Error Handling Pattern
func main() {
var wg sync.WaitGroup
errCh := make(chan error, 3)
urls := []string{
"https://example.com",
"https://invalid-url",
"https://httpbin.org",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
errCh <- fmt.Errorf("failed %s: %w", u, err)
return
}
resp.Body.Close()
fmt.Printf("%s: %s\n", u, resp.Status)
}(url)
}
wg.Wait()
close(errCh)
for err := range errCh {
fmt.Println("Error:", err)
}
}
Parallel Map
func parallelMap[T any, R any](items []T, fn func(T) R) []R {
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
go func(idx int, val T) {
defer wg.Done()
results[idx] = fn(val)
}(i, item)
}
wg.Wait()
return results
}
func main() {
nums := []int{1, 2, 3, 4, 5}
squares := parallelMap(nums, func(n int) int { return n * n })
fmt.Println(squares)
}
Batch Processing
func processBatch(items []int, batchSize int) {
var wg sync.WaitGroup
for i := 0; i < len(items); i += batchSize {
end := i + batchSize
if end > len(items) {
end = len(items)
}
wg.Add(1)
go func(batch []int) {
defer wg.Done()
for _, item := range batch {
fmt.Printf("Processing %d\n", item*2)
}
}(items[i:end])
}
wg.Wait()
}
func main() {
items := make([]int, 20)
for i := range items { items[i] = i + 1 }
processBatch(items, 5)
}
Common Mistakes
1. Adding Inside Goroutine
go func() {
wg.Add(1) // Race condition! Add before go
defer wg.Done()
}()
2. Negative Counter
wg.Add(1)
wg.Done()
wg.Done() // Panic: negative WaitGroup counter
3. Not Calling Wait
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); doWork() }()
// If we don't call wg.Wait(), the program may exit before the goroutine finishes
4. Copying WaitGroup
func worker(wg sync.WaitGroup) { // Copy! Should use pointer
defer wg.Done()
}
5. Forgetting Defer Done
go func() {
// wg.Done() // Missing! WaitGroup will wait forever
doWork()
wg.Done() // Won't run if doWork panics
}()
Practice Questions
1. What is the counter in WaitGroup? Starts at 0. Add(n) increments. Done() decrements. Wait() blocks until counter reaches 0.
2. Can WaitGroup be reused? Yes, but only after Wait() has returned. All goroutines must complete before reusing.
3. How do you pass WaitGroup to functions? As a pointer *sync.WaitGroup. Never copy a WaitGroup.
4. What happens if Add is called after Wait? It's safe if done within the same goroutine before Wait. Add from other goroutines after Wait is a race.
Challenge: Write a function that fetches multiple URLs concurrently and collects all errors.
Solution
func fetchAll(urls []string) []error {
var wg sync.WaitGroup
var mu sync.Mutex
var errs []error
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
return
}
resp.Body.Close()
}(url)
}
wg.Wait()
return errs
}
FAQ
{{< faq question="When should I use WaitGroup vs channels?" >}} WaitGroup when you need to wait for completion. Channels when you need to communicate results. Often used together. {{< /faq >}}
{{< faq question="Can I use WaitGroup with a fixed number of goroutines?" >}} Yes. Call Add once with the total count before launching goroutines. Done() in each goroutine decrements. {{< /faq >}}
{{< faq question="Is WaitGroup safe for concurrent use?" >}} Yes. WaitGroup is designed for concurrent access. The Add/Done/Wait methods are thread-safe. {{< /faq >}}
{{< faq question="What is WaitGroup zero value good for?" >}}
The zero value is ready to use. You can declare var wg sync.WaitGroup without any initialization.
{{< /faq >}}
{{< faq question="How do I handle panics in goroutines with WaitGroup?" >}}
Always use defer wg.Done(). If you need to recover, add defer func() { if r := recover(); r != nil { /* handle */ } }().
{{< /faq >}}
Try It Yourself
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Goroutine %d done\n", id)
}(i)
}
wg.Wait()
fmt.Println("All done")
}
Expected output (order may vary):
Goroutine 0 done
Goroutine 1 done
Goroutine 2 done
All done
What's Next
Now that you understand WaitGroups, learn about Mutexes for protecting shared state.
| Topic | Description | Link |
|---|---|---|
| Go Mutexes | Shared state protection | {{< ref "21-mutexes" >}} |
| Go Channels | Channel communication | {{< ref "18-channels" >}} |
| Go Concurrency Patterns | Advanced patterns | {{< ref "24-concurrency-patterns" >}} |