Recipe

Go Race Detector Primer

The Go race detector is a runtime instrumentation tool that flags concurrent reads and writes to the same memory location without proper synchronization. It is one of the cheapest ways to catch entire categories of concurrency bugs before they reach production, and Meridian builds ship with it enabled in CI.

1. What it actually catches

The race detector watches every memory access at runtime and tracks the happens-before relationship between goroutines. When two goroutines touch the same address and at least one of those touches is a write, with no synchronization edge between them, a warning fires. It does not detect deadlocks, leaked goroutines, or logical race conditions hidden behind a mutex. It catches exactly one thing extremely well: unsynchronized shared state.

2. How to enable it

Pass -race to any go build, go test, or go run invocation. The compiler swaps in an instrumented runtime. Expect roughly 5x to 10x memory overhead and a 2x to 20x slowdown, which is why production binaries usually omit the flag.

# Build with the race detector enabled
go build -race ./...

# Run tests with the race detector
go test -race ./...

# Run a binary with the race detector
go run -race main.go

# Example race condition output
==================
WARNING: DATA RACE
Read at 0x00c0000a4010 by goroutine 7:
  main.increment()
      /app/main.go:14 +0x44

Previous write at 0x00c0000a4010 by goroutine 6:
  main.increment()
      /app/main.go:14 +0x5a
==================

3. Reading a race report

Every report has two stack traces: the offending access and the previous conflicting access. Start at the bottom write, identify the shared variable, then walk the read stack to find the consumer. The fix is almost always one of three patterns: wrap the variable in a sync.Mutex, replace it with a channel, or use a sync/atomic primitive when the field is a single word.