Data races and the race detector

Go · Advanced · 10 min

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 . or go build -race for 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/atomic types 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.

Check yourself

  1. The race detector reports nothing for your test suite. What can you conclude?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/doc/articles/race_detector