A mutex does not protect shared state if each goroutine locks a different copy of it. The Go standard library’s sync package provides mutexes, condition variables, wait groups, and other synchronization primitives. They coordinate concurrent work, but their behavior depends on sharing the same primitive. section 9.14
type Counter struct {
mu sync.Mutex
counters map[string]int
}
func NewCounter() Counter { #1
return Counter{counters: map[string]int{}}
}
func (c Counter) Increment(name string) {
c.mu.Lock() #2
defer c.mu.Unlock()
c.counters[name]++
}
The value receiver on Increment copies Counter, including its sync.Mutex. Concurrent calls then lock separate mutexes while changing the same map. section 9.14
Do not copy a sync value after it is in use; this applies to sync.Cond, sync.Map, sync.Mutex, sync.RWMutex, sync.Once, sync.Pool, and sync.WaitGroup. For the counter, choose a fix that preserves a shared lock:
- Use a pointer receiver, func (c *Counter) Increment(name string), so the method call does not copy Counter. section 9.14
- If a value receiver is needed, store mu as *sync.Mutex and initialize it. An omitted pointer field is nil, so calling Lock on it panics. section 9.14
The package also helps when locking alone is not enough. Suppose one goroutine increments a donation balance while separate listeners wait for goals of $10 and $15. Repeatedly unlocking and relocking an RWMutex to check the balance creates a CPU-wasting busy loop. Sending each update on one shared channel is not a broadcast: only one listener receives each message, so the $10 listener might first observe $11. section 9.12
type Donation struct {
cond *sync.Cond #1
balance int
}
donation := &Donation{
cond: sync.NewCond(&sync.Mutex{}), #2
}
// Listener goroutines
f := func(goal int) {
donation.cond.L.Lock()
for donation.balance < goal {
donation.cond.Wait() #3
}
fmt.Printf("%d$ goal reached\n", donation.balance)
donation.cond.L.Unlock()
}
go f(10)
go f(15)
// Updater goroutine
for {
time.Sleep(time.Second)
donation.cond.L.Lock()
donation.balance++ #4
donation.cond.L.Unlock()
donation.cond.Broadcast() #5
}
A sync.Cond pairs the balance with a lock. Each listener checks its goal in a loop around Wait; the updater changes the balance under the lock and calls Broadcast to notify the listeners. section 9.12
Use the shared condition variable when multiple listeners need to recheck their own conditions after an update. Use a shared channel with multiple receivers only when delivery to one receiver per message fits the task. When a lock appears ineffective, check for copied sync values: go test -race exposes the counter’s data race, and go vet reports that Increment passes a lock by value. section 9.12 section 9.14


