Go

How does the race detector work, what does it cost, and what are its limits?

Question 399MediumGo 1.22 to 1.25

go test -race / go build -race instruments every memory access and uses ThreadSanitizer's happens-before (vector clock) algorithm. It reports when two goroutines access the same location, at least one writes, and no synchronization (mutex, channel, atomic, WaitGroup) orders them. Reports include both stacks and where the goroutines were created.

func TestCounterRace(t *testing.T) {
    var n int
    var wg sync.WaitGroup
    for range 100 {
        wg.Add(1)
        go func() {
            defer wg.Done()
            n++ // WARNING: DATA RACE
        }()
    }
    wg.Wait()
    _ = n
}
// Fix: var n atomic.Int64; n.Add(1)  -- or protect with a sync.Mutex

Costs: roughly 5–10x memory and 2–20x CPU; before Go 1.19 there was a hard limit of 8128 simultaneously alive goroutines; Go 1.19 moved to ThreadSanitizer v3, which removed that limit and made the detector roughly 1.5–2x faster with half the memory. Supported only on 64-bit platforms (linux, darwin, windows, freebsd amd64/arm64 and a few others).

Limits: it's dynamic — it only finds races on code paths that actually execute concurrently during that run. No false positives, but false negatives are common. It doesn't detect deadlocks, logical races (check-then-act with each step locked), or leaks. So: run CI with -race, write tests that exercise concurrency (t.Parallel, RunParallel), and optionally run a canary instance with -race under real traffic. Gotcha: "benign" races don't exist in Go — the memory model allows a racy program to observe torn writes, and racy map access crashes the process with "concurrent map writes".

More on Performance, Profiling & Testing

All 38 Performance, Profiling & Testing questions