← Debugging Guides

阅读中文版本

Go — data race detected by the race detector

Read a Go race report, identify the conflicting accesses, and fix the synchronization with a mutex, channel, or atomic rather than ignoring the warning.

advanceddebugginggoconcurrency

Symptoms

  • WARNING: DATA RACE
  • The race detector prints two goroutine stacks with Read at and Previous write at
  • The bug appears only under `go test -race` or high concurrency
  • State occasionally becomes inconsistent without a panic

Possible causes

  • Two goroutines access a shared variable without synchronization
  • A map is read and written concurrently
  • A loop variable is captured by a goroutine
  • A struct field is updated while another goroutine reads it
  • A channel or WaitGroup is used incorrectly, so a goroutine reads before initialization finishes

Diagnosis steps

Reproduce with the race detector enabled

go test -race ./...

Interpretation

The report identifies the two conflicting accesses and the goroutines involved. The first report is usually the most actionable.

Map both stacks to the shared value

go test -race -run TestName -v ./pkg

Interpretation

Identify the variable and the synchronization boundary. If one access is under a mutex and the other is not, the lock scope is wrong.

Check ownership and lifecycle

grep -n 'go func\|sync\.\|atomic\.\|chan ' path/to/file.go

Interpretation

Look for goroutines that outlive the data they capture, or writes that happen after a WaitGroup signal.

Root cause

Two goroutines accessed the same memory concurrently without a happens-before relationship. Go's memory model does not guarantee ordering in that case, even if the program appears to work.

Fix

Protect the shared value with a mutex or use a channel to transfer ownership. Use atomic operations for simple counters. Ensure goroutines do not outlive the data they access, and close or signal channels only after the final write.

Risk note

Removing `-race` from CI hides the report but not the bug. A data race can corrupt memory, produce torn reads, or crash the runtime unpredictably.

Verify the fix

Run the failing test repeatedly with `-race -count=100`, then run the full suite with the race detector. Confirm the report is gone and the behavior is deterministic. Add the concurrency test to CI.

The report is a map, not noise

A race report shows two accesses to the same memory with no happens-before relationship. One stack is the current read or write, the other is the previous conflicting access. Read both. If one side holds a mutex and the other does not, the lock is not protecting that path.

WARNING: DATA RACE
Read at 0x00c000... by goroutine 7:
  main.(*Counter).Value()
Previous write at 0x00c000... by goroutine 6:
  main.(*Counter).Add()

Choose a synchronization primitive

A mutex protects a critical section; a channel transfers ownership and can make the sharing explicit; atomics work for simple counters and flags. Do not mix a mutex with an unsynchronized read “for speed.” For maps, either guard every access with the same lock or use a concurrency-safe structure. For loop variables, pass the value into the goroutine or create a per-iteration variable.

Reproduce and keep it fixed

Run go test -race -count=100 on the failing test. A race may be rare; repetition and high concurrency increase the chance of detection. Keep the race-enabled suite in CI, at least for packages with shared state. If the race cannot be reproduced, document the suspected path and add a stress test rather than closing the issue.