Skip to content

Day 068 — The Race Detector (-race)

Month 3 · Week 2 · ⬅ Day 067 · Day 069 ➡ · Journal index

🎯 Learning Objective

Catch data races with Go's -race flag, read the report it prints, and fold -race into the normal test workflow — while understanding its limits.

📚 Topics

  • go run/test/build -race, what it instruments, and the cost
  • Reading a race report (the two conflicting accesses + goroutine creation stacks)
  • Why -race only finds races that actually happen at runtime

📖 Reading / Sources

📝 Notes

  • The [[race-detector]] is built into the toolchain: add -race to go run, go test, go build, or go install. It compiles your program with ThreadSanitizer instrumentation that watches memory accesses at runtime.
  • It is a dynamic detector: it only flags a race if a racy interleaving actually occurs during this run. No report ≠ race-free; it cannot prove absence. Run it under realistic load and in tests with concurrency.
  • A race report shows two stacks (the conflicting Read and Write), where each goroutine was created, and the variable involved. The fix is to add synchronisation (mutex/atomic/channel) so the two accesses are ordered.
  • Cost: roughly 2–20× slower and 5–10× more memory; instrumentation is significant, so run -race in CI/tests, not in production binaries.
  • -race finds data races (unsynchronised concurrent access), not logical races, deadlocks, or leaks. Deadlocks surface as a runtime "all goroutines are asleep" panic; leaks need other tooling (goroutine dumps, goleak-style checks).
  • Make it routine: go test -race ./.... Concurrency bugs are non-deterministic and hide from a plain go test; the detector is how you surface them deterministically-ish.
  • Pair -race with go vet (catches copied locks, lost context cancels, bad Printf verbs) — static + dynamic together cover most concurrency footguns.

💻 Code Examples

$ go run -race ./examples/month-03/mutex   # with the lock removed
==================
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 8:
  main.(*SafeCounter).Inc()
      .../mutex/main.go:34 +0x...
Previous write at 0x00c0000b4010 by goroutine 7:
  main.(*SafeCounter).Inc()
      .../mutex/main.go:34 +0x...
Goroutine 8 (running) created at:
  main.main()
      .../mutex/main.go:71 +0x...
==================
Found 1 data race(s)
exit status 66

Reproduce: delete the Lock/Unlock in examples/month-03/mutex/main.go and run go run -race ./examples/month-03/mutex. With the lock in place the run is clean.

🏋️ Exercises / Practice

Exercise Status Link
Re-run all week-2 tests under -race go test -race ./exercises/month-03/week-2/...

🐛 Mistakes Made

  • Assumed a clean go test meant my code was race-free; adding -race immediately surfaced an unguarded append. Lesson: -race is the real check.
  • Tried to ship a -race binary "to be safe" — it was far slower and heavier. -race belongs in tests/CI, not production.

❓ Open Questions

  • How do I deterministically force a rare interleaving the detector keeps missing? (Stress loops, t.Parallel, GOMAXPROCS>1, and runtime.Gosched() to widen windows.)

🧠 Active Recall (answer without looking)

  1. Q: Does a clean go test -race run prove your code has no data races?
    A

No. The detector is dynamic — it only reports races on interleavings that actually occurred this run. It can't prove absence; exercise concurrency under load to raise confidence. 2. Q: Will -race catch a deadlock or a goroutine leak?

A

No. -race finds data races only. A full deadlock triggers the runtime's "all goroutines are asleep" panic; leaks need goroutine-dump/leak tooling.

🪶 Feynman Reflection

The race detector is a wiretap on memory: it watches who reads and writes each address and shouts when two goroutines touch the same spot without a "happens-before" handshake. It only hears the calls that actually happen, so a quiet wiretap means "none seen", not "none exist".

🕳️ Knowledge Gaps

  • The precise happens-before edges -race reasons about — exactly tomorrow's memory-model topic.

✅ Summary

I run go test -race ./... as routine, can read the two-stack race report and fix it with synchronisation, and I know -race is a dynamic detector for data races only — not a proof, and not for production builds.

⏭️ Next Steps / Prep for Tomorrow

  • Day 069: the Go memory model — the happens-before rules that make today's synchronisation correct.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦🟦⬜

Suggested commit: docs(journal): the race detector and -race workflow (day 068)