Race Detector
Purpose and Scope
The Go race detector is a build-mode feature for finding data races in Go programs. A data race occurs when two goroutines access the same memory concurrently and at least one access is a write, without synchronization that establishes an ordering between them. For users, the entry point is the -race flag on commands such as go test -race, go run -race, go build -race, and go install -race. For implementers, race-enabled builds expose runtime hooks and internal helper functions that let selected packages annotate memory accesses and synchronization events.
This page explains how that integration is represented in the Go source tree. The important distinction is between the user-facing detector and the internal instrumentation surface. User programs generally do not call these runtime functions directly; the compiler, runtime, and selected internal packages cooperate to report conflicting accesses. The repository files here show the conditional build structure: race builds link real hooks to the runtime, while non-race builds compile the same internal API as no-ops so ordinary builds do not pay the same cost or require the race runtime.
Sources: src/internal/race/doc.go, src/internal/race/norace.go, src/internal/race/race.go, src/runtime/race.go
Relevant Source Files
src/internal/race/doc.go- Defines the intent of packageinternal/race: helper functions for manually instrumenting code for the race detector, exported unconditionally by this internal package even though runtime exports exist only in race builds.src/internal/race/norace.go- Provides the!raceimplementation of the internal helper API. It setsEnabledtofalse, implements instrumentation functions as no-ops, and returns zero fromErrors.src/internal/race/race.go- Provides theraceimplementation of the same internal helper API. It setsEnabledtotrueand usesgo:linknamedeclarations to bind helper names to runtime functions.src/runtime/race.go- Defines the runtime-side race API present only in race builds, including memory access hooks, range hooks, synchronization annotations, enable/disable control, and error counting.src/internal/runtime/sys/consts_race.go- Records a race-build constant in the internal runtime system package by definingisRace = 1under theracebuild tag.src/cmd/trace/main.go- Implementsgo tool trace, a complementary diagnostics tool that reads execution traces, serves a browser UI, and can generate pprof-like profiles for synchronization, scheduling, syscall, and network blocking.
Build-Tag Model
The race detector appears in the tree as a build-tagged feature. Files with //go:build race are compiled only for race-enabled builds, while //go:build !race files provide the ordinary implementation. This pattern keeps the internal package shape stable across builds. Code can import internal/race, call functions such as Read, Write, Acquire, or Release, and branch on race.Enabled without needing separate call sites for race and non-race binaries. In non-race builds those calls become empty helper functions, preserving compatibility while avoiding detector work.
The non-race file is deliberately complete rather than absent. It defines every helper name exposed by the race build: access instrumentation, program-counter variants, object-aware variants, range instrumentation, synchronization annotations, enable/disable controls, and Errors. The result is a small but important contract. Internal code can be written once, and the build system selects whether those operations are meaningful. Errors returning zero in the non-race file also gives callers a consistent way to ask whether the detector has observed reports without needing a separate build-conditional wrapper.
Sources: src/internal/race/norace.go, src/internal/race/race.go
Runtime and Internal API Components
The internal/race package is the repository-local facade for manual race instrumentation. Its documentation says the runtime package intentionally exports these functions only in the race build, while this internal package exports them unconditionally and makes them no-ops without the race build tag. That wording captures the design goal: keep public surface area limited, but still let internal packages express detector-specific events. Because internal/race is not an external public package, it can serve the standard library and runtime implementation without becoming a supported API for arbitrary modules.
In a race build, src/internal/race/race.go declares the helper functions without bodies and attaches //go:linkname directives. These declarations bridge names such as internal/race.Read, internal/race.Write, internal/race.Acquire, and internal/race.Errors to runtime-provided implementations. The file also imports internal/abi for object-aware access helpers and unsafe for memory addresses. The Enabled constant is true, so internal code can choose race-specific paths at compile time when needed.
Sources: src/internal/race/doc.go, src/internal/race/race.go
src/runtime/race.go is the runtime side of that bridge. It declares low-level public race functions that exist only in race builds, such as RaceRead, RaceWrite, RaceReadRange, and RaceWriteRange, then exposes linked wrappers named for internal/race. It also defines RaceErrors, which calls into the ThreadSanitizer report-count hook and returns the number of race reports as an integer. Synchronization functions such as RaceAcquire, RaceRelease, and RaceReleaseMerge annotate happens-before edges that the detector might not otherwise observe.
The synchronization API deserves special attention because it explains what manual instrumentation is for. Normal Go synchronization, such as channel operations and mutexes, is visible to the runtime and detector through other instrumentation. Some runtime or internal code, however, may implement synchronization in ways that need explicit annotation. RaceAcquire and RaceRelease model acquire and release operations, and RaceReleaseMerge merges with earlier releases. RaceDisable and RaceEnable temporarily suppress handling of synchronization events in the current goroutine, while memory accesses and function entry or exit can still affect detector state.
Sources: src/runtime/race.go
Execution Flow in a Race-Enabled Build
A typical race-detection workflow starts outside these files: the developer builds or tests with -race, runs code under a realistic workload, and reads reports that include conflicting access stacks and goroutine creation stacks. Once a race-enabled binary is being built, the race build tag selects the runtime and internal helper implementations shown here. The compiler and runtime cooperate to call detector hooks around memory operations and synchronization events. When a selected internal package makes manual calls through internal/race, those calls reach the runtime by go:linkname.
In a non-race build, the same helper calls remain source-compatible but have no detector behavior. internal/race.Enabled is false, all access and synchronization functions return immediately, and Errors reports zero. In a race build, Enabled is true, helper calls are linked to runtime functions, and the runtime can delegate to the underlying ThreadSanitizer integration. The internal runtime system constant isRace = 1 gives runtime-adjacent code a race-build marker at the internal/runtime/sys layer, reinforcing that race mode is a build-wide property rather than a dynamically toggled option.
Sources: src/internal/race/norace.go, src/internal/race/race.go, src/internal/runtime/sys/consts_race.go, src/runtime/race.go
Compact Reference
| Component | Build condition | Contract |
|---|---|---|
internal/race.Enabled | race or !race | Compile-time boolean indicating whether helper calls are active. |
internal/race.Read, Write | both | Annotate single-address memory reads and writes; no-op outside race builds. |
internal/race.ReadRange, WriteRange | both | Annotate memory range accesses; no-op outside race builds. |
internal/race.ReadPC, WritePC | both | Annotate accesses with caller and program counter information. |
internal/race.ReadObjectPC, WriteObjectPC | both | Annotate typed object accesses using *abi.Type. |
internal/race.Acquire, Release, ReleaseMerge | both | Describe synchronization edges for the detector; no-op outside race builds. |
internal/race.Disable, Enable | both | Suppress and restore synchronization-event handling in the current goroutine in race builds. |
internal/race.Errors | both | Return detector report count in race builds and zero otherwise. |
runtime.RaceRead, RaceWrite, RaceReadRange, RaceWriteRange | race | Runtime-side detector entry points present only in race builds. |
runtime.RaceAcquire, RaceRelease, RaceReleaseMerge | race | Runtime-side happens-before annotations. |
runtime.RaceDisable, RaceEnable | race | Runtime-side nested synchronization-ignore controls. |
internal/runtime/sys.isRace | race | Internal runtime-system constant set to 1 for race builds. |
The reference table should be read as an implementation contract, not as end-user API guidance. The internal/race package is for code inside the Go source tree, and runtime/race.go is guarded by the race build tag. Application developers should normally rely on go test -race and related go command modes instead of importing internal packages or depending on linked runtime symbol names.
Sources: src/internal/race/norace.go, src/internal/race/race.go, src/runtime/race.go, src/internal/runtime/sys/consts_race.go
Relationship to Trace and Other Diagnostics
Race detection and execution tracing solve different debugging problems, but they are often used in the same investigation. Race detection reports unsynchronized conflicting memory accesses. Execution tracing, implemented by go tool trace, helps explain scheduling, blocking, syscall, synchronization, and network behavior over time. The trace command accepts a trace file produced by go test -trace=trace.out pkg, opens a browser UI, and can emit pprof-like profiles using -pprof=net, -pprof=sync, -pprof=syscall, or -pprof=sched.
That separation matters when choosing a diagnostic path. If a test intermittently corrupts state or fails with impossible values, start with -race so the detector can identify conflicting accesses. If the program is correct but slow, stuck, or dominated by blocking, capture an execution trace and inspect scheduler or synchronization profiles. The trace tool’s main program parses flags, opens the trace file, dispatches profile generation when -pprof is set, and otherwise serves the trace viewer over HTTP. Together, these tools cover complementary concurrency debugging workflows in the Go distribution.
Sources: src/cmd/trace/main.go
Practical Workflow and Next Steps
For application code, the practical workflow is intentionally simple: run tests with the detector enabled, exercise realistic concurrent paths, and treat any reported data race as a correctness bug. A minimal command sequence is go test -race ./... for a module or workspace, go run -race . for an executable during local reproduction, and go build -race ./cmd/name when you need to run a longer scenario outside the test harness. Because the detector observes executed code paths, coverage of the workload matters as much as the flag itself.
For contributors working inside the Go tree, the source files on this page explain how to add or review manual detector annotations. Prefer ordinary synchronization primitives when possible, because they are already visible to the runtime. Use the internal helper facade when runtime-adjacent code needs to tell the detector about memory accesses or happens-before relationships it cannot infer. When debugging concurrency behavior beyond races, move next to execution traces and profiles: go tool trace can expose blocking and scheduling structure that a race report does not attempt to show.
Related pages: memory-model-and-synchronization, testing, execution-traces, diagnostics