Data Races and Mutexes
Data Races and Mutexes
Overview
When multiple goroutines access the same variable and at least one writes, you have a data race unless synchronization creates happens-before. Maps can fatal on concurrent write; other races may silently corrupt.
Concurrent map writes
counter := map[string]int{}
// two goroutines: counter[word]++ → often:
// fatal error: concurrent map writescounter[word]++ is not one atomic step (hash, read, add, write, maybe grow).
Race detector
go run -race .
go test -race ./...WARNING: DATA RACE
Read at ... by goroutine 6
Previous write at ... by goroutine 7
Races are timing-dependent—passing once means nothing. Always CI with -race on supported platforms (needs CGO toolchain historically).
Channels are race-free for their send/receive protocol; sharing the payload pointers can still race.
Fix 2: mutex
var mu sync.Mutex
mu.Lock()
counter[word]++
mu.Unlock()sequence (top → bottom):
actors: G1, Mutex, G2
G1 --> Mutex : Lock
G2 --> Mutex : Lock (blocks)
G1 --> G1 : critical section
G1 --> Mutex : Unlock
Mutex --> G2 : acquired
G2 --> G2 : critical section
G2 --> Mutex : Unlock
Use mutex when:
- Multiple writers, or
- Writer + readers without other sync
Readers-only → no mutex needed for that data.
Non-reentrant
mu.Lock()
mu.Lock() // deadlock — Go mutex is not reentrantPass by pointer
Same rule as WaitGroup—copies break exclusion.
RWMutex
var rw sync.RWMutex
// readers
rw.RLock(); _ = m[k]; rw.RUnlock()
// writer
rw.Lock(); m[k]=v; rw.Unlock()Many concurrent readers; writers exclusive. Measure—RWMutex is not always faster.
Writer W Readers R1 R2 R3 R4
Mutex: W excludes everyone; each R serializes
RWMutex: R1–R4 may overlap; W waits for readers
Critical section hygiene
| Do | Don’t |
|---|---|
| Lock only around shared data | Hold lock across HTTP/DB |
| Unlock via defer when multi-return | Forget unlock on error path |
| Document what mu protects | One global mutex for everything |
Runnable example
go run -race .package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
counter := map[string]int{}
var wg sync.WaitGroup
inc := func(word string) {
defer wg.Done()
mu.Lock()
counter[word]++
mu.Unlock()
}
wg.Add(2)
go inc("go")
go inc("go")
wg.Wait()
fmt.Println(counter)
}What to notice: -race is clean; remove mutex and it may fail or warn.
Try next: Rewrite with per-worker maps + merge; compare to mutex under -bench.