Skip to content

Go WaitGroups — Synchronizing Goroutines with sync.WaitGroup

DodaTech Updated 2026-06-28 5 min read

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" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go