The Race Detector and Memory Model
Concurrent bugs are the hardest to find — until you let Go find them for you. You'll learn what a data race really is, how the Go memory model and happens-before decide when writes are visible, how go test -race instruments your code, and how to fix races with mutexes, channels, and sync/atomic .
Learn The Race Detector and Memory Model in our free Go course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ A program with a data race
Here 100 goroutines increment the same plain int with no synchronization. counter++ is a read-modify-write, so updates are lost and the result varies. Run with -race and Go prints a detailed report.
2️⃣ Fix it with a sync.Mutex
A mutex serializes access: Unlock happens-before the next Lock , so only one goroutine touches counter at a time. The result is now always 100 and the detector is silent.
3️⃣ Fix it with sync/atomic
For a single counter, an atomic is lighter than a mutex. atomic.Int64.Add performs a race-free read-modify-write in hardware and also establishes happens-before ordering.
4️⃣ Channels create happens-before too
A channel send happens-before the corresponding receive completes . That ordering makes the goroutine's earlier writes visible after the receive — no mutex required for the handoff.
🎯 Your Turn
Protect the shared sum. Fill in the two blanks marked ___ so the write is guarded, then run with -race .
❌ Using time.Sleep to "fix" a race — it only hides the symptom.
✅ Add real synchronization: mutex, channel, or atomic.
❌ Locking the read but not the write (or vice versa) — still a race.
✅ Guard every access to the shared variable with the same mutex.
❌ Mixing atomic and non-atomic access to the same variable.
✅ Use atomic operations for all reads and writes of it.
❌ Shipping the -race binary to production — big CPU/memory overhead.
✅ Enable -race in tests and CI; ship the normal build.
No. A race needs at least one write. Concurrent reads with no writes are safe.
Send happens-before the receive completes. That makes the sender's earlier writes visible to the receiver.
Use an atomic.Int64 so 200 goroutines can count even indices with no mutex and no data race.
Practice quiz
What is a data race in Go?
- two goroutines accessing the same memory concurrently with at least one write and no synchronization
- any program with more than one goroutine
- a deadlock between two channels
- a slow function that misses a deadline
Answer: two goroutines accessing the same memory concurrently with at least one write and no synchronization. A data race is concurrent access to the same memory where at least one access is a write and there is no happens-before ordering.
How do you enable the race detector when running tests?
- go test -v
- go test -cover
- go test -race
- go test -bench
Answer: go test -race. go test -race instruments the binary to detect races; -race also works with go run, go build, and go install.
What does the Go memory model define?
- the maximum heap size
- the happens-before conditions under which one goroutine's writes are visible to another
- how garbage collection schedules
- the size of an int
Answer: the happens-before conditions under which one goroutine's writes are visible to another. The memory model specifies happens-before guarantees that determine when a read observes a particular write across goroutines.
Which of these establishes a happens-before relationship?
- calling time.Sleep
- printing to stdout
- incrementing a plain int
- sending on a channel before the corresponding receive
Answer: sending on a channel before the corresponding receive. A channel send happens-before the receive completes; mutex Unlock happens-before a later Lock; these create ordering.
How does the race detector find races?
- it instruments memory accesses at runtime and tracks happens-before ordering
- it scans source code statically
- it counts goroutines
- it reads the go.mod file
Answer: it instruments memory accesses at runtime and tracks happens-before ordering. ThreadSanitizer-based instrumentation records each memory access and reports when two are unordered with a write involved.
Which type provides lock-free atomic operations on a counter?
- sync.Once
- sync/atomic (e.g. atomic.Int64)
- time.Timer
- context.Context
Answer: sync/atomic (e.g. atomic.Int64). sync/atomic offers atomic loads, stores, and Add operations; atomic.Int64.Add is a race-free way to update a counter.
A key LIMITATION of the race detector is that it...
- finds every possible race by static analysis
- only works on Windows
- slows code by exactly 2x
- only reports races on code paths that actually execute at runtime
Answer: only reports races on code paths that actually execute at runtime. It is a dynamic tool: it only detects races that actually happen during the run, so good test coverage matters.
Which is a correct way to fix a race on a shared counter?
- run with more CPUs
- add a time.Sleep before the write
- guard it with a sync.Mutex around read and write
- make the variable global
Answer: guard it with a sync.Mutex around read and write. A sync.Mutex (Lock/Unlock) serializes access; channels and sync/atomic are other valid fixes. Sleeping does not fix races.
Does the race detector add overhead?
- no, it is free
- yes — typically several times more CPU and memory, so it is for testing/CI not production
- it makes code faster
- only on the first run
Answer: yes — typically several times more CPU and memory, so it is for testing/CI not production. Instrumentation increases CPU and memory use significantly, so you enable -race in tests and CI, not in production binaries.
Why is a data race undefined behavior even if the program 'seems to work'?
- the compiler and hardware may reorder or tear accesses, giving torn or stale values unpredictably
- Go converts it to a deadlock
- races only matter in C, not Go
- it always crashes immediately
Answer: the compiler and hardware may reorder or tear accesses, giving torn or stale values unpredictably. Without happens-before ordering, reads may observe partial or stale writes; the behavior is undefined and platform-dependent.