Not everything fits channels; WaitGroup and Mutex are the simplest correct tools for many jobs.
sync.WaitGroup waits for a set of goroutines to finish:
wg.Add(n)before starting the goroutines (in the launching goroutine, not inside the new one).defer wg.Done()at the top of each goroutine.wg.Wait()blocks until the counter reaches zero.
(Go 1.25 adds wg.Go(f), which does the Add/Done bookkeeping for you.)
sync.Mutex gives mutual exclusion around shared state: mu.Lock(), then defer mu.Unlock(). Embed the mutex next to the data it protects, in the same struct, and keep critical sections short — never hold a lock while doing I/O or waiting on a channel. sync.RWMutex allows many concurrent readers (RLock) or one writer (Lock); it only pays off when reads greatly outnumber writes.
Other useful pieces:
sync.Once(orsync.OnceValue, Go 1.21+) runs initialisation exactly once, even with concurrent callers.sync/atomictypes such asatomic.Int64for simple counters and flags.sync.Mapfor specialised caches; a plain map plus a mutex is usually clearer.
A mutex or WaitGroup must not be copied after first use. Pass structs containing them by pointer and use pointer receivers; go vet flags copies.
When should you use a mutex instead of a channel? Use channels to pass ownership of data or coordinate stages of work; use a mutex when goroutines simply need safe access to a shared structure such as a cache or counter.
Example
package main
import (
"fmt"
"sync"
)
type Stats struct {
mu sync.Mutex
hits map[string]int
}
func (s *Stats) Record(page string) {
s.mu.Lock()
defer s.mu.Unlock()
s.hits[page]++
}
func main() {
s := &Stats{hits: make(map[string]int)}
var wg sync.WaitGroup
for i := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
s.Record([]string{"home", "about"}[i%2])
}()
}
wg.Wait()
fmt.Println(s.hits) // map[about:50 home:50]
}Remember
- WaitGroup: Add before
go,defer Done()inside, Wait to join - Keep the Mutex next to the data it guards; lock briefly
- Never copy a Mutex or WaitGroup — use pointers
Watch out for
- Calling wg.Add(1) inside the goroutine, racing with wg.Wait().