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 .
Common Errors (and the fix)
❌ 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.
🧠 Quick Recall
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.
Frequently Asked Questions
🎯 Mini-Challenge: Atomic Counter
Use an atomic.Int64 so 200 goroutines can count even indices with no mutex and no data race.
🎉 Lesson Complete!
- ✅ A data race = concurrent access, at least one write, no happens-before order
- ✅ go test -race / go run -race instrument and report races at runtime
- ✅ The memory model defines happens-before : channels, mutexes, atomics establish it
- ✅ Fix races with sync.Mutex , channels, or sync/atomic — never time.Sleep
- ✅ The detector only finds races on paths that actually run , so test coverage matters
- ✅ Next lesson: Generic Stdlib: slices, maps & cmp
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.