What does go test -race do, and what happens to the run when it detects a race?
answer
- a build flag, not a linter
- the binary is rebuilt instrumented
- only conflicts that actually execute
- two stacks, then a FAIL line
- the non-zero exit is what CI sees
basics
~20 sgo test -race compiles the tests with the race detector, which watches memory accesses as they run. A conflict prints a WARNING: DATA RACE report naming both accesses with their goroutine stacks, fails that test, and makes go test exit non-zero.
solid answer
~50 s`go test -race` rebuilds the package under test and its dependencies with instrumentation, so every memory access and every synchronisation operation is recorded while the tests execute. It is a dynamic check: it reports a conflict only when both accesses actually run in that particular run, so a race on a path the suite never drives stays invisible. When it fires, the runtime prints a `WARNING: DATA RACE` block with both accesses and their goroutine stacks; the `testing` package notices and fails the running test with `race detected during execution of test`. By default the run keeps going and reports further races, printing `Found N data race(s)` at exit. Either way `go test` prints FAIL and exits non-zero — that non-zero exit is the whole reason the flag is usable as a CI gate rather than just a debugging aid.
code
text · 14 lines$ go test -race ./cache
==================
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 9:
example/cache.(*Store).Set()
/src/cache/store.go:41 +0x64
Previous read at 0x00c0000b4010 by goroutine 8:
example/cache.(*Store).Get()
/src/cache/store.go:29 +0x3c
==================
--- FAIL: TestStoreConcurrent (0.01s)
race detected during execution of test
FAILgo deeper
Be ready to say that -race is passed to go test, that it rebuilds the tests with instrumentation, that the report names both conflicting accesses, and that the run fails. Mention running it locally before pushing concurrent code.
Explain that detection is dynamic — both accesses must really execute — and walk through a report: the two stacks, the goroutine creation sites, and the testing package message that pins the race to one test.
Show how the non-zero exit is what makes the detector a merge gate, and how you would spot a step that silently swallows it. Be able to separate race failures from ordinary failures in a large CI log.
Own what a green race job entitles the organisation to claim. Make sure nobody presents it as proof of correctness, and decide what other evidence you require for code that shares state across goroutines.
## The flag is a build flag, not a runtime switch `-race` changes how the test binary is compiled. The compiler instruments the package under test and everything it links, inserting a call into the race runtime around each memory read and write and around each synchronisation operation (channel send and receive, mutex lock and unlock, atomic operations, `WaitGroup` calls, goroutine start and exit). The instrumented runtime maintains shadow state for the memory the program touches, recording which goroutine last read or wrote each location and what synchronisation had happened at that moment. Because it is a build flag, there is no way to switch it on for an existing binary. `go build -race`, `go run -race` and `go test -race` all produce a different, instrumented artefact, cached separately from the ordinary one. ## What it reports, and what it cannot The detector is *dynamic*. It reports a conflict when two goroutines actually touch the same memory location, at least one of them writing, with no synchronisation ordering the two accesses — and both of those accesses have to genuinely execute during the run. That has two consequences a junior candidate is expected to state plainly: - A race in code your tests never call is not reported. The detector saw nothing, so it says nothing. - A race in code your tests *do* call may still go unreported if, in that run, the two goroutines happened to be ordered by some synchronisation, or one of them never got far enough. Running the same suite again can produce a different verdict. So a green `-race` run is evidence that the interleavings you exercised were clean. It is not a proof of absence. ## Reading the output A report is bracketed by lines of `=` and starts with `WARNING: DATA RACE`. It names the offending access first — for example `Write at 0x... by goroutine 9:` — with a stack, then `Previous read at 0x... by goroutine 8:` with its stack, and often the stack where each goroutine was created. That second stack is the valuable half: it tells you which other code path was touching the same memory. The `testing` package checks after each test whether the race runtime recorded new reports, and if so fails that test with the message `race detected during execution of test`. That is how a race becomes attributable to a specific test rather than to the package as a whole. If a race is detected outside any running test — during package initialisation, in `TestMain` before or after `m.Run`, or in a goroutine that outlived the test that started it — there is no test to blame, and you see only the report plus `Found N data race(s)` at exit. ## The exit status By default the run does not stop at the first report; it continues, collecting more, and the race runtime uses a dedicated exit status (66 unless you override it through the `GORACE` environment variable) when the process exits because races were detected. From `go test`'s point of view the package failed: it prints `FAIL` and exits non-zero. Any CI system that treats a non-zero exit as a failed step therefore gates merges on the detector with no extra wiring — and conversely, a pipeline step that swallows that status (piping it through something that always succeeds, or only grepping the log) has silently turned the gate off. ## How you actually use it Run it locally on the package you just changed before you push, especially if the change introduced a goroutine, a shared map or a cached value: `go test -race ./cache`. When a report appears, read both stacks, find the shared location, and decide whether the fix is a mutex, a channel hand-off, giving each goroutine its own copy, or removing the sharing entirely. Re-run under `-race` to confirm the report is gone; the absence of a report on the same test that produced one is meaningful, even though absence in general is not. A common beginner mistake is to reach for `-race` when the symptom is a hang or a leak. It is a data-race detector: it has nothing to say about a deadlock, a goroutine that never exits, or a logic bug in your synchronisation that is nonetheless properly synchronised.
- Does a clean go test -race run prove the package has no data races?No. The detector only observes accesses that execute in that run, so a race in a branch the tests never drive — or one whose two accesses happened to be ordered by synchronisation this time — is simply not reported. A green run says the interleavings you exercised were clean, nothing more.
- How do you tell a race failure apart from an ordinary assertion failure in a noisy log?A race failure carries a `WARNING: DATA RACE` block delimited by lines of `=`, and the failing test's message is `race detected during execution of test` rather than text you wrote. At exit you also see `Found N data race(s)`. An ordinary failure shows only your own `t.Errorf` or `t.Fatalf` message.
- Can you use the race detector on something other than tests?Yes. `go build -race` and `go run -race` produce the same instrumentation for an ordinary program, which is how you exercise a service under a load generator or a staging soak rather than only under unit tests. The cost is the same, so you would not ship an instrumented binary to production.
The race detector is a witness, not an auditor. It can testify in detail about the conflicting accesses it actually watched happen, and it has nothing at all to say about the ones that did not occur while it was watching.
saying these in an interview costs you the question
- Says -race statically analyses the source for unsynchronised access
- Claims a green -race run proves the code is race-free
- Thinks a detected race only warns and leaves the exit status zero
- Expects -race to find deadlocks or leaked goroutines
- Believes -race is a runtime flag needing no rebuild