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!

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.

Continue this course

Related lessons