What does go test -race do, and what does its DATA RACE report contain?
answer
- a different binary, not a different test run
- the compiler wraps every shared access
- ordering comes from channels, mutexes, atomics
- the report names both accesses
- and where each goroutine was created
basics
~20 sgo test -race builds an instrumented test binary that watches memory accesses while the tests run. When two goroutines touch the same memory unsynchronized and at least one writes, it prints WARNING: DATA RACE with a stack for each access, and the test fails.
solid answer
~50 s`-race` is a build mode: the compiler wraps memory reads and writes with calls into a race runtime that is linked into the test binary, so `go test -race` runs a different binary from plain `go test`. While the tests execute, that runtime records which goroutine touched each piece of memory and how those accesses were ordered by real synchronization — channel operations, `sync.Mutex`, `sync.WaitGroup`, `sync/atomic`. If two accesses to the same memory are not ordered by any of that and one of them is a write, it prints a `WARNING: DATA RACE` block naming both accesses: the write, the previous conflicting access, a full stack for each, and the line where each goroutine was created. The test is marked failed, so a race in CI is a red build rather than a warning nobody reads.
code
text · 16 lines==================
WARNING: DATA RACE
Write at 0x00c0000a4010 by goroutine 9:
main.(*collector).add()
/app/collector.go:21 +0x44
Previous read at 0x00c0000a4010 by goroutine 8:
main.(*collector).flush()
/app/collector.go:33 +0x38
Goroutine 9 (running) created at:
main.(*collector).start()
/app/collector.go:14 +0x9c
==================
Found 1 data race(s)
FAILgo deeper
Be ready to say what the flag does in one breath: instrumented build, watches real memory accesses, prints both conflicting accesses with stacks, fails the test. Knowing the invocation go test -race ./... is expected.
Explain where the ordering comes from — channel operations, mutex lock and unlock, WaitGroup, atomics — and that the detector compares that ordering, not the clock, so accesses seconds apart are still reported.
Show that you use it as a first move, not a last resort, and that you read the goroutine-creation frames rather than only the access stacks, because that is usually where the sharing was introduced.
Own the position that a race build is a separate, expensive artifact with its own runtime budget, and be able to say what a green -race job does and does not license the team to claim.
## The flag is a build mode, not a test option `-race` is accepted by `go build`, `go run`, `go install` and `go test` alike. When you pass it, the compiler instruments the code: around reads and writes of memory that could be shared, it inserts calls into a race runtime that is linked into the resulting binary. The binary `go test -race` runs is therefore a genuinely different program from the one plain `go test` runs — same source, different object code, cached separately. Everything people say about the race detector's speed and memory use applies to that instrumented binary only; it says nothing about the build you deploy. ## What it watches while the tests run For each word of memory the program touches, the race runtime keeps a small record of which goroutines accessed it and how those goroutines were ordered relative to each other. Ordering comes from the synchronization you already write: a channel send and the receive that takes it, a `sync.Mutex` unlock and the next lock of the same mutex, `sync.WaitGroup.Done` and the `Wait` that returns after it, `sync.Once`, and the `sync/atomic` operations. When a new access arrives, the runtime asks one question: is this access ordered, through some chain of those operations, with respect to the earlier accesses to this same memory? If two accesses to the same memory are **not** ordered by anything, and at least one of them is a write, that is a data race and it is reported. Notice what is *not* in that rule: the wall-clock gap between the two accesses. Two goroutines that touch the same variable a full second apart are still reported, as long as nothing synchronizes them. You do not have to be lucky enough to hit the exact interleaving that would corrupt data — you only have to make both accesses happen in the same run. ## Reading the report A report is a block delimited by lines of `=` characters. It names the offending access first (`Write at 0x... by goroutine 9`), then the earlier conflicting one (`Previous read at 0x... by goroutine 8`), each with a stack trace pointing at file and line. Below those, `Goroutine 9 (running) created at:` gives the stack of the `go` statement that started that goroutine — often more useful than the access itself, because it shows where the sharing was introduced. At the end of the run the binary prints `Found N data race(s)` and the test fails. The address matters: the report is about a piece of memory, not a variable name. Two differently named slices that share one backing array are the same memory as far as the detector is concerned. ## What it costs An instrumented binary typically runs 2–20x slower and uses 5–10x more memory than the same tests without `-race`. That is why `-race` is a deliberate mode rather than the default, and why bolting it onto an existing CI job with tight timeouts and memory limits tends to produce timeouts before it produces race reports. ## What it is not It is not a static analyser. It never looks at code it does not execute, so it is not a substitute for review or for linting, and a clean run says only that the accesses that actually happened were properly ordered. It is also not a general concurrency checker. It does not report deadlocks — a program where every goroutine is blocked is caught by the Go runtime's own fatal `all goroutines are asleep - deadlock!` throw, not by `-race`. It does not report goroutines that never exit. And it does not judge whether your locking is *semantically* right: code that takes a mutex around each of two operations that needed to be atomic together is perfectly race-free and still wrong. ## Running it `go test -race ./...` is the usual invocation, optionally narrowed with `-run` while you are chasing one test. Because the failure is a normal test failure, it fits any CI system without special handling. The one habit worth forming early: when a concurrency bug is reported and you cannot reproduce it, running the relevant tests under `-race` is the first thing to try, not the last.
- Does a passing go test -race run mean the package has no data races?No. It is a runtime detector: it only sees memory accesses that actually executed in that run. A racy path guarded by an error branch, a retry, or a config flag that no test exercises is simply never observed. A green run means the accesses that happened were properly ordered — nothing more.
- Will go test -race catch a deadlock or a leaked goroutine?No. `-race` reports unsynchronized concurrent accesses only. A program where every goroutine is blocked triggers the Go runtime's own fatal `all goroutines are asleep - deadlock!` throw, and goroutines that never exit have to be found another way, such as sampling `runtime.NumGoroutine` or taking a goroutine profile.
- Why not just build everything with -race all the time?The instrumented binary typically runs 2–20x slower and uses 5–10x more memory, because every shared read and write now calls into the race runtime and the runtime keeps per-word access history. That is fine for a test job and unacceptable for most production workloads, so `-race` stays a deliberate build mode.
It is a wiretap on memory rather than a code review: it only hears the conversations that actually take place while it is listening, but for those it reports both speakers.
saying these in an interview costs you the question
- Calls it a static analysis pass over the source
- Thinks it proves the package is race-free
- Believes the two accesses must overlap in time
- Expects it to report deadlocks or goroutine leaks
- Assumes the -race binary is the one you ship