Data Races and -race
A data race is two goroutines touching the same variable with no ordering between them; -race finds them at runtime.
go test -race ./...A data race happens when two goroutines access the same variable at the same time, at least one of them writes, and nothing orders the two accesses. The result is not a wrong number you can reason about. The program's behaviour is undefined: it can lose updates, read half-written values, or corrupt a map and crash.
Races are the hardest kind of bug to find by reading code, because the program usually works. It fails on a busier machine, or once a week in production.
The detector
Go ships a race detector. Add the -race flag to go test, go run or go build, and the compiler instruments every memory access. When two goroutines touch the same address without ordering, the program prints a report with both stacks and the goroutine that started them.
go test -race ./...
go run -race .Instrumented binaries run several times slower and use much more memory, so you build them for tests and debugging, not for release. go test -race ./... belongs in CI: the cost is one slower job, and the alternative is finding the race from a customer's crash.
What trips people up
The detector reports only races that actually happen during that run. A clean run proves nothing about code the test never exercised. Races are found by tests that run concurrent paths, not by the flag alone.
Example
package main
import (
"fmt"
"sync"
)
func main() {
nums := []int{3, 5, 7, 9}
// Each goroutine writes only to partial[i]. Different elements are
// different memory addresses, so there is nothing shared to race on
// and `go run -race .` reports nothing.
partial := make([]int, len(nums))
var wg sync.WaitGroup
for i, n := range nums {
wg.Go(func() {
partial[i] = n * 2
})
}
wg.Wait()
// The combining step runs in one goroutine, after Wait, so the
// total is the same every run.
total := 0
for _, p := range partial {
total += p
}
fmt.Println("total:", total)
}
// Output:
// total: 48When to use it
- Add -race to the test job in CI so unsynchronised shared state fails the build.
- Reproduce an intermittent production wrong-total bug by running the handler's test under -race.
- Check that a cache you just made concurrent is actually safe before shipping it.
More examples
The race this replaces
Run this with go run -race . and the detector prints "WARNING: DATA RACE", the line doing the write, the line doing the conflicting read, and where each goroutine was created. Without -race the program usually prints a plausible-looking wrong number. The real fixes are a mutex, an atomic counter, or per-goroutine slots as in the main example.
package main
import (
"fmt"
"sync"
)
func main() {
total := 0 // one variable, written by every goroutine
var wg sync.WaitGroup
for i := 1; i <= 1000; i++ {
wg.Go(func() {
// Read, add, write back. Two goroutines can read the same
// old value and one update is lost.
total += i
})
}
wg.Wait()
// Prints 500500 only by luck; often less, and never reliably.
fmt.Println(total)
}A race test worth running in CI
The assertion catches lost updates; the -race flag catches the unsynchronised access even on a run where the total happens to come out right. In CI use go test -race -count=1 ./... so a cached pass from an earlier commit cannot stand in for a real run.
package counter
import (
"sync"
"testing"
)
// TestConcurrentAdd exercises the concurrent path on purpose, because
// -race can only report a race it actually observes.
func TestConcurrentAdd(t *testing.T) {
c := New()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Go(func() {
c.Add(1)
})
}
wg.Wait()
if got := c.Value(); got != 100 {
t.Fatalf("Value() = %d, want 100", got)
}
}Maps crash with or without the flag
The runtime has its own cheap detector for concurrent map access, and unlike a race report it kills the process immediately and cannot be recovered. If a map is reachable from more than one goroutine it needs a mutex around every access, or each goroutine needs its own map that you merge at the end.
package main
import "sync"
func main() {
seen := map[string]int{}
var wg sync.WaitGroup
for _, k := range []string{"a", "b", "c"} {
wg.Go(func() {
// Two goroutines writing one map can abort the program with
// "fatal error: concurrent map writes". This check is in the
// runtime itself, so it fires in production builds too.
seen[k]++
})
}
wg.Wait()
}
Discussion