Synchronization: sync and atomic¶
Channels are Go's preferred way to coordinate goroutines, but sometimes you
just need to protect a piece of shared state. The sync and sync/atomic
packages provide the classic tools: mutexes, one-time initialisation, and
lock-free counters.
The problem: data races¶
When two goroutines touch the same variable and at least one writes, without synchronisation, the result is a data race — undefined behaviour. Imagine a page-hit counter bumped by 1000 concurrent requests — this looks like it reaches 1000, but doesn't reliably:
hits := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); hits++ }() // RACE: concurrent writes
}
wg.Wait()
fmt.Println(hits) // unpredictable: often < 1000
hits++ is read-modify-write — three steps that can interleave and lose
updates, so some requests get "counted" on top of each other.
sync.Mutex: mutual exclusion¶
A Mutex lets only one goroutine into the guarded section at a time. Lock
before touching the shared state, Unlock after (usually via defer).
var mu sync.Mutex
hits := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
hits++ // one request counted at a time
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(hits) // output: 1000
Now the increments are serialised, so every request is counted — always 1000.
sync.RWMutex: many readers or one writer¶
When reads vastly outnumber writes, an RWMutex lets any number of readers
proceed in parallel (RLock/RUnlock) while writes (Lock/Unlock) get
exclusive access.
var mu sync.RWMutex
balance := 0
var wg sync.WaitGroup
// one writer
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
mu.Lock() // exclusive
balance++
mu.Unlock()
}
}()
// three concurrent readers
for r := 0; r < 3; r++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
mu.RLock() // shared — readers don't block each other
_ = balance
mu.RUnlock()
}
}()
}
wg.Wait()
fmt.Println(balance) // output: 100
The readers run in parallel with each other; only the writer's Lock
forces everyone else to wait. Run it with go run -race and it's clean.
sync.Once: run exactly once¶
Once.Do(f) runs f a single time, no matter how many goroutines call it
or how often — the standard way to do lazy, thread-safe initialisation.
var once sync.Once
setup := func() { fmt.Println("init") }
for i := 0; i < 3; i++ {
once.Do(setup)
}
// output:
// init
sync/atomic: lock-free counters¶
For a single integer, an atomic type is simpler and faster than a mutex.
The typed atomics (atomic.Int64, atomic.Bool, …) carry their own
synchronisation:
var hits atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); hits.Add(1) }()
}
wg.Wait()
fmt.Println(hits.Load()) // output: 1000
Reach for atomics for simple counters and flags; reach for a mutex when you must keep several values consistent together.
The race detector¶
Go ships a race detector that instruments memory access and reports
races at runtime. Run your program or tests with -race:
Running the racy count++ loop from the top of this article with -race
reports it:
$ go run -race .
==================
WARNING: DATA RACE
Read at 0x00c0000a0068 by goroutine 8:
main.main.func1()
Previous write at 0x00c0000a0068 by goroutine 9:
main.main.func1()
==================
It only catches races that actually occur during the run, so use it with
tests that exercise concurrency. It's one of the most valuable tools in
Go — make a habit of running tests with -race in CI.
From Python: there's no GIL serialising bytecode, so Go code really does race. The flip side: real parallelism, plus a first-class detector to catch the mistakes the GIL would have hidden.
Quick reference¶
| Tool | Use |
|---|---|
sync.Mutex (Lock/Unlock) |
exclusive access to shared state |
sync.RWMutex (RLock/RUnlock) |
many readers or one writer |
sync.Once (Do) |
run an init exactly once |
sync.WaitGroup |
wait for goroutines to finish |
atomic.Int64 etc. (Add/Load/Store) |
lock-free counters & flags |
go run -race / go test -race |
detect data races |