Menu
Coddy logo textTech

sync.WaitGroup

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 64 of 107.

In the Goroutines Basics lesson, we used time.Sleep to wait for goroutines to finish—a fragile approach. sync.WaitGroup provides a proper way to wait for a collection of goroutines to complete their work.

A WaitGroup works like a counter. You increment it before starting a goroutine, decrement it when the goroutine finishes, and block until the counter reaches zero:

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 3; i++ {
        wg.Add(1)  // increment counter
        go func(id int) {
            defer wg.Done()  // decrement when done
            fmt.Printf("Worker %d finished\n", id)
        }(i)
    }
    
    wg.Wait()  // block until counter is 0
    fmt.Println("All workers complete")
}

The three key methods are Add(n) to increase the counter by n, Done() to decrease it by one (equivalent to Add(-1)), and Wait() to block until the counter reaches zero. Using defer wg.Done() ensures the counter decrements even if the goroutine panics.

A common mistake is calling Add inside the goroutine instead of before it. This creates a race condition where Wait might return before all goroutines are registered:

// Wrong - race condition
go func() {
    wg.Add(1)  // might run after Wait()
    defer wg.Done()
}()

// Correct - Add before launching
wg.Add(1)
go func() {
    defer wg.Done()
}()

WaitGroups are often embedded in structs to coordinate concurrent operations, making them essential for building thread-safe types in Go.

challenge icon

Challenge

Easy

Let's build a download manager that coordinates multiple concurrent file downloads using sync.WaitGroup. Your manager will track when all downloads complete without relying on arbitrary sleep timers.

You'll organize your code across two files:

  • downloader.go: Define your download coordination logic.

    Create a Download struct with ID (int), Filename (string), and Size (int, representing KB) fields.

    Implement a method Process(wg *sync.WaitGroup) on Download that simulates downloading by sleeping for a duration based on the file size (use Size * 10 milliseconds). When the download completes, print: Downloaded: [Filename] ([Size]KB). Use defer wg.Done() to ensure the WaitGroup counter decrements properly.

    Create a function StartDownloads(downloads []Download) int that launches all downloads concurrently using goroutines and waits for them all to complete. Remember to call wg.Add(1) before launching each goroutine, not inside it. Return the total size of all downloads combined.

  • main.go: Read download information and orchestrate the concurrent downloads.

    Read the number of downloads, then for each download read its ID, filename, and size. Create the downloads and pass them to StartDownloads. After all downloads complete, print: All downloads complete: [total]KB total

The following inputs will be provided:

  • Line 1: Number of downloads (integer)
  • Following lines: For each download, three lines - the ID (integer), filename (string), and size in KB (integer)

For example, given:

3
1
report.pdf
20
2
image.png
5
3
data.csv
10

Your output should show downloads completing (smaller files finish first due to shorter sleep times), followed by the summary:

Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB total

The key difference from using time.Sleep is that wg.Wait() blocks until all goroutines signal completion with Done(), giving you precise synchronization regardless of how long each download takes.

Cheat sheet

A sync.WaitGroup provides a proper way to wait for goroutines to complete. It works like a counter that tracks running goroutines.

The three key methods are:

  • Add(n) - increases the counter by n
  • Done() - decreases the counter by one (equivalent to Add(-1))
  • Wait() - blocks until the counter reaches zero

Basic usage pattern:

var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
    wg.Add(1)  // increment before starting goroutine
    go func(id int) {
        defer wg.Done()  // decrement when done
        fmt.Printf("Worker %d finished\n", id)
    }(i)
}

wg.Wait()  // block until all goroutines complete
fmt.Println("All workers complete")

Using defer wg.Done() ensures the counter decrements even if the goroutine panics.

Important: Always call Add before launching the goroutine, not inside it, to avoid race conditions:

// Wrong - race condition
go func() {
    wg.Add(1)  // might run after Wait()
    defer wg.Done()
}()

// Correct
wg.Add(1)
go func() {
    defer wg.Done()
}()

Try it yourself

package main

import (
	"fmt"
)

func main() {
	// Read the number of downloads
	var n int
	fmt.Scanln(&n)

	// Read download information
	downloads := make([]Download, n)
	for i := 0; i < n; i++ {
		var id int
		var filename string
		var size int
		fmt.Scanln(&id)
		fmt.Scanln(&filename)
		fmt.Scanln(&size)
		
		downloads[i] = Download{
			ID:       id,
			Filename: filename,
			Size:     size,
		}
	}

	// TODO: Call StartDownloads with the downloads slice
	// TODO: Print the summary: "All downloads complete: [total]KB total"
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming