Race Detector Internals
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.Done → Wait 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.goWhat 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.