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:
UPS 1240 USPS 1095 FedEx 1310 took 400ms
How it works
wg.Add(1)counts one more job. Call it before thegostatement.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.