Using Go's Race Detector to Find and Fix Data Races
Learn how to enable Go's built‑in race detector, spot a data race in a simple counter example, and fix it with a mutex, while understanding the detector’s performance cost and blind spots.
05 Jun 2026, 14:41 UTC

Problem: intermittent counter corruption
You notice that a service occasionally reports a wrong count after many goroutines update a shared variable. The failure is hard to reproduce because it depends on the timing of concurrent accesses.
Enabling the race detector
Go’s toolchain includes a built‑in race detector based on ThreadSanitizer. Add the -race flag to any go build, go run, or go test command. No extra installation is required.
Worked example: a racing counter
Consider this minimal program that increments a global counter from two goroutines without synchronization:
package main
import "fmt"
var counter int
func increment() {
counter++ // unsafe access
}
func main() {
go increment()
go increment()
// give goroutines time to finish
// (in real code you would use a WaitGroup)
fmt.Println(counter)
}
Build and run with the detector:
go run -race main.go
You will see output that includes a warning about a data race, showing the conflicting goroutine stacks and the source line where the unsynchronized increment occurs. The detector reports the race because at least one access is a write and there is no happens‑before ordering.
Fixing the race with a mutex
Adding a sync.Mutex around the increment eliminates the race:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
increment()
}()
go func() {
defer wg.Done()
increment()
}()
wg.Wait()
fmt.Println(counter)
}
Running the same command now produces no race report, confirming that the mutex establishes the required happens‑before relationship.
Trade‑offs and limitations
- Performance: enabling
-racetypically makes execution 2‑5× slower and increases memory usage due to shadow metadata. - Coverage: the detector only reports races that actually happen during the run; untested code paths can hide races.
- Scope: it does not detect logical races, deadlocks, or races that involve only read‑only accesses.
To verify that the detector is active, look for the -race flag in the build output and note that the resulting binary is larger than a build without the flag.
Closing
Make -race a regular part of your test suite (go test -race ./...) to catch concurrency bugs early. Treat any race report as a cue to introduce proper synchronization—mutexes, channels, or atomic operations—and re‑run the detector to confirm the issue is gone. Remember that the race detector complements, but does not replace, careful design, code review, and stress testing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.