← Back to Go Course | Chapter 11: Concurrency | Lesson 5 of 8

sync.WaitGroup

A WaitGroup is like a checklist that counts how many workers are still busy, and it lets the boss wait until that count drops to zero before moving on.
Syntax
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    // work
}()
wg.Wait()

Waiting for Goroutines to Finish

sync.WaitGroup lets the main goroutine wait until a group of other goroutines have all finished, replacing fragile approaches like time.Sleep with a precise, deterministic signal.

Example: Waiting for Goroutines to Finish

markup
// Every Go file belongs to a package; main builds an executable
package main

// Import the packages this program uses
import (
	"fmt"
	"sync"
)

// main is where the program starts running
func main() {
	// Declare wg with the zero value of its type
	var wg sync.WaitGroup
	// Loop: repeat while the condition holds
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		// Start this call in a new goroutine (runs concurrently)
		go func(n int) {
			// defer runs this call when the surrounding function returns
			defer wg.Done()
			// Print the values followed by a newline
			fmt.Println("task", n, "complete")
		}(i)
	}
	wg.Wait()
	fmt.Println("all tasks finished")
}

Add Before, Done Inside

Add(1) should be called once for each goroutine right before it's launched, from the main goroutine -- calling it from inside the new goroutine risks Wait() returning before Add has even run.

Note: Always defer wg.Done() as the very first line inside the goroutine, so it runs even if the goroutine panics.

Example: Add Before, Done Inside

markup
// Every Go file belongs to a package; main builds an executable
package main

// Import the packages this program uses
import (
	"fmt"
	"sync"
)

// Define the function process
func process(id int, wg *sync.WaitGroup) {
	// defer runs this call when the surrounding function returns
	defer wg.Done()
	// Print the values followed by a newline
	fmt.Println("processing item", id)
}

// main is where the program starts running
func main() {
	// Declare wg with the zero value of its type
	var wg sync.WaitGroup
	// Loop: repeat while the condition holds
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go process(i, &wg)
	}
	wg.Wait()
	fmt.Println("done")
}

Passing WaitGroup by Pointer

A sync.WaitGroup must never be copied after first use -- always pass it around as a pointer (*sync.WaitGroup), since copying it would give different parts of the code independent, inconsistent counters.

Example: Passing WaitGroup by Pointer

markup
// Every Go file belongs to a package; main builds an executable
package main

// Import the packages this program uses
import (
	"fmt"
	"sync"
)

// Define the function worker
func worker(wg *sync.WaitGroup, results *[]int, mu *sync.Mutex, n int) {
	// defer runs this call when the surrounding function returns
	defer wg.Done()
	mu.Lock()
	*results = append(*results, n*n)
	mu.Unlock()
}

// main is where the program starts running
func main() {
	// Declare wg with the zero value of its type
	var wg sync.WaitGroup
	// Declare mu with the zero value of its type
	var mu sync.Mutex
	// Short declaration: create results and infer the type
	results := []int{}
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go worker(&wg, &results, &mu, i)
	}
	wg.Wait()
	fmt.Println(results)
}
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Calling wg.Add() inside the goroutine instead of before launching it, creating a race on the counter's initial value.
  2. Forgetting to call wg.Done() (often via defer) inside each goroutine, causing Wait() to block forever.
  3. Copying a sync.WaitGroup by value into a function instead of passing a pointer, which silently breaks its counting.
Chapter Summary
  • sync.WaitGroup tracks a count of outstanding goroutines to wait for.
  • Add(n) increases the counter, Done() decrements it, and Wait() blocks until it reaches zero.
  • Call Add() before launching each goroutine, not from inside it, to avoid a race.
  • Always pass a *sync.WaitGroup (or hold it by reference) -- copying it breaks the count.
🔒

Chapter Quiz — Complete all 8 topics to unlock

0/8 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.