Correct lock-free and concurrent code depends on exactly which operations order memory between goroutines.
The Go memory model answers one question: when is a goroutine guaranteed to see a write made by another? Without synchronisation, never — compilers and CPUs reorder and cache memory operations freely, as long as a single goroutine can't tell.
Guarantees come from happens-before edges created by synchronising operations:
go f()happens beforefstarts.- A send on a channel happens before the corresponding receive completes; closing a channel happens before a receive that returns because it's closed.
- For an unbuffered channel, a receive happens before the matching send completes. Generally, the k-th receive on a channel with capacity C happens before the (k+C)-th send completes — which is why a buffered channel works as a semaphore.
mu.Unlock()happens before the nextmu.Lock()returns.once.Do(f): f's completion happens before anyDoreturns.wg.Donehappens before theWaitit unblocks.sync/atomicoperations are sequentially consistent: if an atomic load observes an atomic store, the store happens before the load.
Happens-before is transitive, so writes before a send are visible after the receive.
Since the 2022 revision (Go 1.19), Go promises DRF-SC: a program without data races behaves as if goroutines were interleaved on one core. With a race, guarantees mostly evaporate. Single-word races only expose some value that was written, but races on strings, slices, interfaces or maps can tear multi-word values and corrupt memory.
The classic broken idiom is a plain bool "ready" flag with a busy loop: the reader may never see the update, or see the flag before the data. Use a channel, a mutex, or an atomic.Bool — then both the flag and the data written before it become visible.
Example
package main
import (
"fmt"
"sync/atomic"
)
var (
data string
ready atomic.Bool // plain bool here would be a data race
)
func producer() {
data = "hello" // (1) ordinary write
ready.Store(true) // (2) atomic store publishes (1)
}
func main() {
go producer()
for !ready.Load() { // (3) observes (2), so (1) happens before (4)
}
fmt.Println(data) // (4) guaranteed: hello
}Remember
- Visibility across goroutines requires a happens-before edge
- Channels, mutexes, Once, WaitGroup and atomics create those edges
- Race-free programs are sequentially consistent; racy ones have no reliable meaning
Watch out for
- Publishing data with a plain bool flag and a spin loop — the reader may never see it, or see stale data.