Learn Go← Dashboard

Mutex, RWMutex, WaitGroup, Once, atomic, errgroup.

Mutex and WaitGroup

Channels are the high-level concurrency tool. But sometimes you really do want plain "share state, lock it" — and that's what the sync package is for.

sync.Mutex

A mutex protects shared state. Lock before reading or writing, unlock when done (usually via defer).

go playground
Loading...

Without the mutex, multiple goroutines would race on c.n++ and you'd get a nondeterministic answer (and go test -race would scream). With it, the increments are serialized.

sync.RWMutex

If your shared state is read often and written rarely, an RWMutex lets many readers in at once but only one writer:

mu.RLock(); v := cache[key]; mu.RUnlock()
mu.Lock();  cache[key] = newVal; mu.Unlock()

Use a plain Mutex first; only switch to RWMutex if profiling shows lock contention. The bookkeeping in RWMutex makes it slower for low-contention cases.

sync.WaitGroup

We saw WaitGroup already — three methods:

  • wg.Add(n) — "expect n more goroutines to finish".
  • wg.Done() — "I'm done". Subtract one.
  • wg.Wait() — block until the counter is zero.

The pattern is always the same:

var wg sync.WaitGroup
for _, x := range work {
    wg.Add(1)
    go func(x Work) {
        defer wg.Done()
        process(x)
    }(x)
}
wg.Wait()

Add happens before the go, so the counter is high before any goroutine has a chance to start. Done always runs (because of defer), even if process panics.

Channels or mutex — when do I use which?

Use a channel to pass ownership of data between goroutines, or to signal events. Use a mutex to protect shared state read and written by several goroutines.

| Need | Reach for | | ------------------------------------------ | -------------- | | Pipeline of data through stages | channel | | Many goroutines incrementing a counter | mutex (or atomic) | | Signaling "this is done" | chan struct{} or WaitGroup | | Caching with multiple readers | RWMutex | | Bounding concurrency (semaphore) | buffered channel |

The classic mistake is reaching for a mutex when a channel would be simpler, or vice versa. Whichever you choose, stick with one model per piece of state.

Why is `defer mu.Unlock()` the standard pattern after `mu.Lock()`?