To wait for multiple goroutines to finish, you can use a wait group from the sync package. WaitGroups provide a clean way to coordinate goroutine completion.
Basic WaitGroup Usage
package main
import (
"fmt"
"sync"
"time"
)
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
// Launch several goroutines
for i := 1; i <= 5; i++ {
wg.Go(func() {
worker(i)
})
}
// Wait for all goroutines to complete
wg.Wait()
fmt.Println("All workers done")
}
The WaitGroup.Go() method automatically handles Add(1) and Done() for you, making it the recommended way to launch goroutines.
WaitGroup Methods
Add(delta int)
Increment the WaitGroup counter:
var wg sync.WaitGroup
wg.Add(1) // Increment by 1
wg.Add(5) // Increment by 5
Done()
Decrement the counter by 1:
go func() {
defer wg.Done() // Decrement when goroutine finishes
// Do work
}()
Wait()
Block until counter reaches zero:
wg.Wait() // Blocks until all Done() calls complete
Go(func())
Launch a goroutine with automatic Add/Done (Go 1.21+):
wg.Go(func() {
// Work is automatically wrapped with Add(1) and Done()
doWork()
})
Traditional Pattern (Before Go 1.21)
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment counter
go func(id int) {
defer wg.Done() // Decrement when done
worker(id)
}(i)
}
wg.Wait() // Wait for all to finish
Always use defer wg.Done() to ensure the counter is decremented even if the goroutine panics.
Modern Pattern (Go 1.21+)
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Go(func() {
worker(i) // Add/Done handled automatically
})
}
wg.Wait()
Passing WaitGroup to Functions
When passing a WaitGroup to a function, always pass it by pointer, never by value.
// CORRECT: Pass by pointer
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg) // Pass pointer
}
wg.Wait()
}
// WRONG: Passing by value creates a copy
func worker(id int, wg sync.WaitGroup) { // BAD!
defer wg.Done() // Decrements the COPY, not original
fmt.Printf("Worker %d\n", id)
}
Common Patterns
1. Fixed Number of Workers
var wg sync.WaitGroup
numWorkers := 10
for i := 0; i < numWorkers; i++ {
wg.Go(func() {
doWork()
})
}
wg.Wait()
2. Processing a Slice
items := []Item{item1, item2, item3}
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() {
process(item)
})
}
wg.Wait()
3. Worker Pool with WaitGroup
func processJobs(jobs []Job, numWorkers int) {
var wg sync.WaitGroup
jobChan := make(chan Job, len(jobs))
// Start workers
for i := 0; i < numWorkers; i++ {
wg.Go(func() {
for job := range jobChan {
job.Execute()
}
})
}
// Send jobs
for _, job := range jobs {
jobChan <- job
}
close(jobChan)
// Wait for all workers
wg.Wait()
}
4. Nested WaitGroups
func processInBatches(items []Item) {
var outerWg sync.WaitGroup
batchSize := 10
for i := 0; i < len(items); i += batchSize {
batch := items[i:min(i+batchSize, len(items))]
outerWg.Go(func() {
var innerWg sync.WaitGroup
for _, item := range batch {
innerWg.Go(func() {
process(item)
})
}
innerWg.Wait() // Wait for batch
})
}
outerWg.Wait() // Wait for all batches
}
Error Handling with WaitGroups
WaitGroups don’t provide built-in error handling. Here are common approaches:
1. Collect Errors in Slice
var (
wg sync.WaitGroup
mu sync.Mutex
errs []error
)
for _, item := range items {
wg.Go(func() {
if err := process(item); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
})
}
wg.Wait()
if len(errs) > 0 {
fmt.Printf("Encountered %d errors\n", len(errs))
}
2. Use Error Channel
var wg sync.WaitGroup
errorChan := make(chan error, len(items))
for _, item := range items {
wg.Go(func() {
if err := process(item); err != nil {
errorChan <- err
}
})
}
wg.Wait()
close(errorChan)
for err := range errorChan {
fmt.Println("Error:", err)
}
3. Use errgroup Package
import "golang.org/x/sync/errgroup"
func processItems(items []Item) error {
var g errgroup.Group
for _, item := range items {
g.Go(func() error {
return process(item)
})
}
// Returns first error encountered
return g.Wait()
}
For advanced error handling, use the errgroup package from golang.org/x/sync/errgroup. It provides WaitGroup functionality with error propagation.
Practical Examples
Concurrent HTTP Requests
func fetchURLs(urls []string) map[string]string {
var wg sync.WaitGroup
var mu sync.Mutex
results := make(map[string]string)
for _, url := range urls {
wg.Go(func() {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
mu.Lock()
results[url] = string(body)
mu.Unlock()
})
}
wg.Wait()
return results
}
File Processing
func processFiles(filenames []string) error {
var (
wg sync.WaitGroup
mu sync.Mutex
errs []error
)
for _, filename := range filenames {
wg.Go(func() {
if err := processFile(filename); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
})
}
wg.Wait()
if len(errs) > 0 {
return fmt.Errorf("failed to process %d files", len(errs))
}
return nil
}
Database Queries
func executeQueries(queries []string, db *sql.DB) []QueryResult {
var (
wg sync.WaitGroup
mu sync.Mutex
results []QueryResult
)
for _, query := range queries {
wg.Go(func() {
rows, err := db.Query(query)
if err != nil {
mu.Lock()
results = append(results, QueryResult{Error: err})
mu.Unlock()
return
}
defer rows.Close()
data := scanRows(rows)
mu.Lock()
results = append(results, QueryResult{Data: data})
mu.Unlock()
})
}
wg.Wait()
return results
}
WaitGroup with Context
func processWithContext(ctx context.Context, items []Item) error {
var wg sync.WaitGroup
errorChan := make(chan error, len(items))
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
wg.Go(func() {
if err := processItem(ctx, item); err != nil {
errorChan <- err
}
})
}
wg.Wait()
close(errorChan)
for err := range errorChan {
if err != nil {
return err
}
}
return nil
}
Common Mistakes
Adding Inside Goroutine
// BAD: Race condition
for i := 0; i < 5; i++ {
go func() {
wg.Add(1) // TOO LATE! May race with Wait()
defer wg.Done()
doWork()
}()
}
wg.Wait()
// GOOD: Add before launching
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
// BEST: Use wg.Go() (Go 1.21+)
for i := 0; i < 5; i++ {
wg.Go(doWork)
}
wg.Wait()
Passing by Value
// BAD: Copies WaitGroup
func worker(wg sync.WaitGroup) { // Passed by value!
defer wg.Done() // Decrements copy, not original
}
// GOOD: Pass by pointer
func worker(wg *sync.WaitGroup) {
defer wg.Done() // Decrements original
}
Reusing WaitGroup
// BAD: Counter may not be zero
var wg sync.WaitGroup
wg.Add(5)
// ... goroutines ...
wg.Wait()
wg.Add(3) // Unsafe if previous goroutines still running!
// GOOD: Create new WaitGroup
wg1 := &sync.WaitGroup{}
wg1.Add(5)
// ... batch 1 ...
wg1.Wait()
wg2 := &sync.WaitGroup{}
wg2.Add(3)
// ... batch 2 ...
wg2.Wait()
Forgetting Done()
// BAD: Deadlock - Wait() blocks forever
wg.Add(1)
go func() {
// Forgot wg.Done()!
doWork()
}()
wg.Wait() // Blocks forever
// GOOD: Always use defer
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
WaitGroups are very lightweight. The overhead per Add/Done operation is ~20-30 nanoseconds.
Benchmarks (approximate)
- Add(): ~20 ns
- Done(): ~20 ns
- Wait(): ~30 ns (when counter is zero)
- Memory: ~24 bytes per WaitGroup
Best Practices
- Use wg.Go() - Simplest and safest (Go 1.21+)
- Add before goroutine - Avoid race conditions
- Pass by pointer - Never pass WaitGroup by value
- Use defer for Done() - Ensures cleanup even on panic
- Consider errgroup - For error handling scenarios
- Create fresh WaitGroups - Don’t reuse across batches
- Document ownership - Make it clear who calls Add/Done/Wait
WaitGroup vs Alternatives
| Pattern | Use When |
|---|
| WaitGroup | Wait for N goroutines to complete |
| Channel | Need result or error from goroutine |
| Context | Need cancellation or timeout |
| errgroup | Need error handling from workers |