Race Detector Internals

Updated

September 8, 2026

Race Detector Internals

Overview

go test -race is not a linter—it is a different ABI/runtime that shadows memory and tracks happens-before. It catches concurrent unsynchronized access with at least one write.

Diagram: instrumentation model

  load/store in user code
           │
           v
  instrumented binary (-race)
           │
     ┌─────┴─────┐
     v           v
  shadow       happens-before
  memory       (unlock, send, …)
     │           │
     └─────┬─────┘
           v
     race? ──yes──► WARNING: DATA RACE

What creates happens-before (reminder)

Event Edge
Unlock → later Lock yes
Channel send → receive yes
WaitGroup.DoneWait return yes
go start parent before child start
Atomics (sync/atomic) model-defined

No edge + concurrent write/read → race report.

Cost model

Mode Overhead
-race tests often 2–10× CPU/mem
Production race binary rarely shipped

Requires CGO toolchain on many platforms historically—CI images need gcc/clang.

False confidence

  • Passing once ≠ no races
  • Coverage matters: races on rare branches need fuzz/load
  • Synctest settles logical concurrency; still run -race

Experiment

cat > /tmp/race.go <<'EOF'
package main
import ("fmt"; "sync")
func main() {
  var x int
  var wg sync.WaitGroup
  wg.Go(func() { x++ })
  wg.Go(func() { x++ })
  wg.Wait()
  fmt.Println(x)
}
EOF
go run -race /tmp/race.go

What to notice: Report names both goroutines and stack sites even if printed total looks “fine”.

Try next: Fix with atomic.Int64 or mutex; confirm clean under -race.