Skip to content

Go goroutines and WaitGroup explained

November 3, 2026 · 2 min read

Start each job with the go keyword, and use a sync.WaitGroup to make main wait until every goroutine has finished. Without the wait, main returns first and the program exits, killing the jobs.

Run three jobs at once

This program asks three carriers for a shipping price. Each quote takes 200 to 400 ms, so running them one after another would take 900 ms.

package main

import (
	"fmt"
	"sync"
	"time"
)

var delays = map[string]time.Duration{
	"UPS": 300 * time.Millisecond, "USPS": 200 * time.Millisecond,
	"FedEx": 400 * time.Millisecond,
}
var prices = map[string]int{"UPS": 1240, "USPS": 1095, "FedEx": 1310}

func quote(carrier string) int {
	time.Sleep(delays[carrier])
	return prices[carrier]
}

func main() {
	start := time.Now()
	carriers := []string{"UPS", "USPS", "FedEx"}
	results := make([]int, len(carriers))

	var wg sync.WaitGroup
	for i, c := range carriers {
		wg.Add(1)
		go func() {
			defer wg.Done()
			results[i] = quote(c)
		}()
	}
	wg.Wait()

	for i, c := range carriers {
		fmt.Println(c, results[i])
	}
	fmt.Println("took", time.Since(start).Round(100*time.Millisecond))
}

Output:

Output
UPS 1240
USPS 1095
FedEx 1310
took 400ms

How it works

  • wg.Add(1) counts one more job. Call it before the go statement.
  • go func() { ... }() runs a function literal as a goroutine, so the loop doesn't wait for it.
  • defer wg.Done() counts the job as finished, even if the function returns early.
  • wg.Wait() blocks until the count is back to zero.

The total is about 400 ms, the slowest job, instead of 900 ms. Each goroutine writes to its own slot in results, so no two goroutines touch the same variable. Results are printed after Wait, which keeps them in carrier order.

Common mistakes

Calling Add inside the goroutine. The goroutine may not have started when Wait runs, so the count is still zero and Wait returns at once. Call wg.Add(1) in the loop, before go.

Passing the WaitGroup by value. A function that takes wg sync.WaitGroup gets a copy, so its Done never reaches the original and Wait blocks forever. Pass *sync.WaitGroup instead.

Takeaway

Add before go, defer Done inside, Wait after the loop. Goroutines run jobs together, and the WaitGroup tells you when they're all done.

More on Go, Rust & C++