What is a race condition, and how does Go's race detector help find them?
Quick Answer
A data race happens when two or more goroutines access the same memory location concurrently, at least one of them is a write, and there's no synchronization ordering the accesses — the result is undefined behavior, not just "maybe the wrong value." Go ships a built-in race detector, enabled with the -race flag on go run, go test, or go build, that instruments memory accesses at runtime and reports the exact goroutines, stack traces, and memory location involved in a detected race. The race detector only catches races that actually occur during a specific run, so it can't prove a program race-free. It only proves a particular execution had no races. Running tests under -race in CI is standard practice precisely because concurrency bugs are often intermittent and hard to reproduce otherwise.
Detailed Answer
A data race in Go isn't just "you might read a stale value." It's genuinely undefined behavior, which is why the race detector is considered essential tooling, not optional.
A race, and what -race reports
func main() {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // unsynchronized read-modify-write: a data race
}()
}
wg.Wait()
fmt.Println(counter) // often not 1000, and the race itself is the real bug
}
go run -race main.go
WARNING: DATA RACE
Read at 0x00c0000140a0 by goroutine 8:
main.main.func1()
main.go:12 +0x3c
Previous write at 0x00c0000140a0 by goroutine 7:
main.main.func1()
main.go:12 +0x52
The detector pinpoints the exact line and both goroutines involved, which is far more actionable than debugging an intermittent wrong answer by hand.
Fixing it
var mu sync.Mutex
counter := 0
// ... inside the goroutine:
mu.Lock()
counter++
mu.Unlock()
Or, for a simple counter, sync/atomic:
var counter int64
atomic.AddInt64(&counter, 1)
The detector's real limitation
-race only flags races that happen during the specific execution you ran. A race that only triggers under a rare goroutine interleaving might not show up in a quick test run, but could still exist in the code. Running the race detector broadly, and running concurrency-heavy tests repeatedly under load, meaningfully increases the odds of catching a race. It's still not a formal proof of race-freedom, the way a static analyzer might aim for.