Waiting with sync.WaitGroup
sync.WaitGroup counts unfinished goroutines so one goroutine can block until all the others have returned.
wg.Add(1); go func() { defer wg.Done(); work() }(); wg.Wait()A goroutine gives you no way to find out when it finished. sync.WaitGroup is the standard answer. It is a counter with three methods: Add raises the count, Done lowers it by one, and Wait blocks until the count reaches zero.
How it works
Declare it with var wg sync.WaitGroup. Its zero value is ready to use, so there is nothing to initialise. Call wg.Add(1) for each goroutine you are about to start, call wg.Done() inside that goroutine, and call wg.Wait() where you need the results. Putting Done behind defer means it still runs if the goroutine returns early or panics.
Shorthand and longhand
Go 1.25 added wg.Go(f), which does all three steps for you. These two are the same thing:
wg.Add(1)
go func() {
defer wg.Done()
work()
}()
wg.Go(work)Older code uses the long form everywhere, so you need to read both.
What trips people up
Call Add before the go statement, never inside the new goroutine. If the goroutine has not run yet when Wait is called, the counter is still zero and Wait returns immediately. A WaitGroup must also be shared, not copied: pass it as *sync.WaitGroup.
Example
package main
import (
"fmt"
"sync"
)
func main() {
nums := []int{1, 2, 3, 4}
squares := make([]int, len(nums))
var wg sync.WaitGroup
// Longhand: count it, start it, mark it done when the body ends.
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
squares[i] = nums[i] * nums[i]
}()
}
// Shorthand, Go 1.25 and later: wg.Go does those same three steps.
for i := 2; i < len(nums); i++ {
wg.Go(func() {
squares[i] = nums[i] * nums[i]
})
}
// Nothing below this line runs until all four goroutines returned.
wg.Wait()
fmt.Println("squares:", squares)
}
// Output:
// squares: [1 4 9 16]When to use it
- Download several files at once and continue only after every download has finished.
- Warm three caches in parallel during start-up before the server accepts traffic.
- Run a fixed pool of workers in a test and assert the results after they all return.
More examples
Pass the WaitGroup as a pointer
This is the single most common WaitGroup bug: a worker function that takes sync.WaitGroup by value. The compiler accepts it and the program hangs. Any function that calls Done takes *sync.WaitGroup.
package main
import (
"fmt"
"sync"
)
// wg must be a pointer. A sync.WaitGroup parameter would copy the
// counter, and Done would lower the copy while Wait watches the
// original, so Wait would block forever.
func fetch(url string, wg *sync.WaitGroup) {
defer wg.Done()
_ = url
}
func main() {
urls := []string{"/a", "/b"}
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go fetch(u, &wg)
}
wg.Wait()
fmt.Println("done")
}Add the whole count once
Add takes a delta, so when you know the number of goroutines up front you can add it in one call before the loop. Use this when the count comes from len(items); keep the per-iteration Add when the loop may skip items.
package main
import (
"fmt"
"sync"
)
func main() {
shards := 3
counts := make([]int, shards)
var wg sync.WaitGroup
// One Add for all three goroutines is allowed; Add takes a delta.
wg.Add(shards)
for i := 0; i < shards; i++ {
go func() {
defer wg.Done()
counts[i] = (i + 1) * 10
}()
}
wg.Wait()
fmt.Println(counts)
}Collecting errors from workers
A WaitGroup carries no results, so each goroutine writes into its own slot and the caller inspects them after Wait. errors.Join (Go 1.20) folds the slice into one error and ignores the nil entries. When you want the first error to cancel the rest, reach for errgroup instead.
package main
import (
"errors"
"fmt"
"sync"
)
func check(n int) error {
if n < 0 {
return errors.New("negative input")
}
return nil
}
func main() {
inputs := []int{4, -1, 7}
// One error slot per worker, so no two goroutines share a variable.
errs := make([]error, len(inputs))
var wg sync.WaitGroup
for i, n := range inputs {
wg.Go(func() {
errs[i] = check(n)
})
}
wg.Wait()
fmt.Println(errors.Join(errs...))
}
Discussion