Races cause rare, unreproducible bugs in production; the race detector catches most of them in testing.
A data race happens when two goroutines access the same memory location concurrently, at least one access is a write, and nothing orders them (no mutex, channel or atomic). Races are not just "lost updates": the compiler and CPU may reorder or cache operations, and a race on a multi-word value — a string, slice, interface or map — can produce torn values and crashes. Concurrent map writes even trigger a fatal runtime error.
Go ships a race detector. Enable it with the -race flag:
go test -race ./...— the most valuable place; run it in CI.go run -race .orgo build -racefor manual testing of a binary.
It instruments memory accesses and reports races that actually happen during the run, with stack traces for both accesses and where the goroutines were created. It has no false positives, but it can only find races your tests exercise — so tests should run the concurrent code paths. Expect roughly 5–10× slower execution and more memory; it's for testing, not production builds. It requires cgo on most platforms.
Fixing a race means establishing happens-before ordering:
- Protect the data with a
sync.Mutex. - Use
sync/atomictypes for single counters or flags. - Transfer ownership through a channel so only one goroutine touches the data at a time.
- Give each goroutine its own data, such as its own slice index, and combine after
wg.Wait().
"It's only a counter, the occasional wrong value is fine" is not a valid excuse — racy programs have no guaranteed behaviour at all.
Example
// counter_test.go — run with: go test -race
package counter
import (
"sync"
"sync/atomic"
"testing"
)
func TestCounter(t *testing.T) {
var racy int
var safe atomic.Int64
var wg sync.WaitGroup
for range 50 {
wg.Add(1)
go func() {
defer wg.Done()
racy++ // WARNING: DATA RACE reported here
safe.Add(1) // atomic: fine
}()
}
wg.Wait()
if safe.Load() != 50 {
t.Fatalf("got %d", safe.Load())
}
_ = racy
}Remember
- A race = concurrent access, one write, no synchronisation
go test -race ./...in CI; it finds races that occur during the run- Fix with mutexes, atomics, channels or not sharing at all
Watch out for
- Assuming no race report means no races — the detector only sees code paths that ran concurrently.