Go Lesson 36 – WaitGroups | Dataplexa

WaitGroups in Go

In the previous lesson, you learned how the select statement helps manage multiple channel operations. In this lesson, you will learn about WaitGroups, which are used to wait for multiple goroutines to finish execution.


What Is a WaitGroup?

A WaitGroup is a synchronization primitive provided by the sync package. It allows one goroutine (usually main) to wait until a group of other goroutines have completed their work.

This is essential when launching multiple goroutines that must finish before the program exits.


Why WaitGroups Are Needed

By default, the main function does not wait for goroutines to complete. If the main function finishes, the program exits—even if goroutines are still running.

WaitGroups solve this problem by blocking execution until all goroutines signal completion.


WaitGroup Methods

  • Add(n) – Adds n goroutines to wait for
  • Done() – Signals that a goroutine has finished
  • Wait() – Blocks until the counter reaches zero

Basic WaitGroup Example

Let’s start with a simple example where the main function waits for two goroutines to finish.

package main

import (
    "fmt"
    "sync"
)

func task(name string, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Task", name, "completed")
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2)

    go task("A", &wg)
    go task("B", &wg)

    wg.Wait()
    fmt.Println("All tasks finished")
}

The program waits until both tasks call Done().


Understanding the Flow

Here’s what happens step by step:

  • Main adds 2 to the WaitGroup counter
  • Two goroutines start running
  • Each goroutine calls Done()
  • The counter reaches zero
  • Wait() unblocks

Real-World Example: Processing Files

Imagine processing multiple files concurrently and waiting until all files are processed.

func processFile(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Processing file", id)
}

func main() {
    var wg sync.WaitGroup

    files := 5
    wg.Add(files)

    for i := 1; i <= files; i++ {
        go processFile(i, &wg)
    }

    wg.Wait()
    fmt.Println("All files processed")
}

This pattern is commonly used in batch jobs and data pipelines.


Using Defer with Done()

It is best practice to call Done() using defer. This ensures the counter is decremented even if an error occurs.

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    // work logic here
}

WaitGroups vs Channels

WaitGroups are designed only for synchronization. They do not transfer data.

  • Use WaitGroups to wait for completion
  • Use channels to send and receive data

In many real systems, both are used together.


Common Mistakes

  • Calling Add() inside goroutines
  • Forgetting to call Done()
  • Reusing a WaitGroup without resetting it
  • Passing WaitGroup by value instead of pointer

WaitGroup with Loop Example

This example launches multiple goroutines dynamically.

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Println("Worker", id)
    }(i)
}

wg.Wait()

Practice Exercises

Exercise 1

Create three goroutines that print messages and wait for all to finish.

Exercise 2

Modify the program to simulate delays using time.Sleep.


Key Takeaways

  • WaitGroups synchronize goroutines
  • Add(), Done(), and Wait() control execution
  • Always pass WaitGroup as a pointer
  • Essential for concurrent workflows

What’s Next?

In the next lesson, you will learn about Mutex Locks and how to protect shared data.