The Go Race Detector

Find data races in Go programs with the -race flag, read the WARNING DATA RACE report, and understand what the race detector can and cannot catch.

A data race happens when two goroutines access the same memory at the same time and at least one of them writes. Races cause wrong results, corrupted maps and crashes that appear only sometimes, often only in production. Go ships with a race detector that finds them for you while your program runs.

A Program With a Race

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	total := 0

	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				total++ // two goroutines write without a lock
			}
		}()
	}
	wg.Wait()
	fmt.Println("total:", total)
}

Run normally with go run ., it may print total: 2000 most of the time and something smaller now and then. Running it once and seeing the right answer proves nothing.

Turning On the Race Detector

Add -race to go run, go test or go build:

go run -race .
go test -race ./...

For the program above, the output looks like this (addresses and goroutine numbers vary):

==================
WARNING: DATA RACE
Read at 0x00c000012128 by goroutine 8:
  main.main.func1()
      /app/main.go:16 +0x84

Previous write at 0x00c000012128 by goroutine 7:
  main.main.func1()
      /app/main.go:16 +0x96

Goroutine 8 (running) created at:
  main.main()
      /app/main.go:13 +0x7c
==================
total: 2000
Found 1 data race(s)
exit status 66

Reading the Report

The report has three useful parts:

  • Read at ... by goroutine 8 and Previous write at ... by goroutine 7: the two conflicting accesses, each with a file and line. Here both point to total++ on line 16.
  • Goroutine ... created at: where the goroutines were started, which helps when the access is deep inside a library.
  • exit status 66: with -race, a program that found a race exits with status 66, so CI jobs fail.

Fix it by protecting total with a sync.Mutex, using atomic.Int64, or giving each goroutine its own counter and adding them up after wg.Wait().

The race detector only sees races that happen:

It watches memory accesses while the program runs. Code paths that your run or tests do not execute are not checked. A clean -race run means no races were seen, not that none exist. Run it on tests that exercise concurrent code paths.

Using -race in Tests and CI

The most effective habit is to run tests with the race detector on every change:

go test -race ./...

Programs built with -race run noticeably slower and use more memory, so it is normally used for tests and staging, not production builds. The race detector needs cgo on some platforms. If you see -race requires cgo; enable cgo by setting CGO_ENABLED=1, set that variable and make sure a C compiler is installed.

Do not silence races with sleeps:

Adding time.Sleep can make a race stop showing up in a test, but the race is still there. Fix the shared access with a lock, an atomic or a channel.

Races the Runtime Catches Without -race

A few races are detected even without the flag, because the runtime checks for them on maps:

fatal error: concurrent map writes
fatal error: concurrent map read and map write

These end the program immediately and cannot be recovered with recover. If you see them, find the map in the stack trace and protect it with a mutex.

Your concurrent code is in good shape when:

go test -race ./... passes in CI on tests that actually run goroutines in parallel, and every shared variable has a clear protection.

Summary

A data race is two goroutines touching the same memory at once with at least one write. Run tests with go test -race ./... to find them. The report gives both conflicting lines and where the goroutines were created, and the program exits with status 66. The detector only finds races that actually happen during the run, so test concurrent paths, and fix races with locks, atomics or channels rather than sleeps.