How do you detect and avoid race conditions in Go?
Detect race conditions in Go with the -race detector and avoid them using channels, sync.Mutex and sync/atomic to synchronize concurrent goroutine access.
Expected Interview Answer
You detect race conditions in Go by running code with the built-in race detector (go run -race, go test -race), and you avoid them by synchronizing access to shared state using channels, sync.Mutex/RWMutex, or sync/atomic operations.
A race condition occurs when two or more goroutines access the same memory concurrently and at least one access is a write, without synchronization. The -race flag instruments memory accesses at runtime and reports the conflicting goroutines and stack traces. To avoid races, either share memory by communicating (pass data over channels so one goroutine owns it) or guard shared variables with a mutex; for simple counters, atomic operations are cheaper than locks.
- Catches nondeterministic bugs before production
- Pinpoints the exact goroutines and lines in conflict
- Encourages clear ownership of shared data
- Enables safe concurrent counters via atomics
- Improves reliability of concurrent programs
AI Mentor Explanation
A race condition is like two batters running to the same crease at once with no call between them — both assume it is safe and a run-out chaos follows. A mutex is the batting partnership's clear call of 'yes' or 'wait' so only one commits at a time. The race detector is the third umpire reviewing replays to catch the exact moment both were unsafely in the same ground.
Step-by-Step Explanation
Step 1
Reproduce under -race
Run go test -race ./... or go run -race main.go to instrument memory accesses at runtime.
Step 2
Read the report
The detector prints the conflicting read/write, both goroutine stacks, and where each was created.
Step 3
Identify shared state
Find the variable, map or slice accessed by multiple goroutines with at least one write.
Step 4
Choose a synchronization strategy
Prefer channels to transfer ownership; use sync.Mutex/RWMutex to guard shared memory; use sync/atomic for counters.
Step 5
Guard every access
Lock/unlock around all reads and writes of the shared variable, or route all access through one owner goroutine.
Step 6
Re-run -race in CI
Keep race-enabled tests in continuous integration to catch regressions early.
What Interviewer Expects
- Defining a race as unsynchronized concurrent access with a write
- Knowing the -race flag and how to run it
- Mentioning channels, sync.Mutex/RWMutex and sync/atomic
- Understanding 'share memory by communicating'
- Awareness that -race adds overhead and needs concurrent execution to trigger
Common Mistakes
- Assuming the race detector catches every race without triggering the code path
- Guarding only writes but leaving reads unsynchronized
- Copying a sync.Mutex by value instead of using a pointer
- Using atomics for compound multi-step invariants that need a lock
- Believing channels alone eliminate all shared-state races
Best Answer (HR Friendly)
“A race condition happens when several parts of a program touch the same data at the same time and at least one changes it, causing unpredictable bugs. In Go you find these by running your tests with the -race flag, and you prevent them by coordinating access using channels or locks so only one goroutine touches the data at a time.”
Code Example
// Racy: multiple goroutines write count without sync
// Run: go run -race main.go -> reports a data race
var count int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() { defer wg.Done(); count++ }()
}
wg.Wait()
// Fixed with a mutex
var mu sync.Mutex
go func() {
mu.Lock()
count++
mu.Unlock()
}()
// Or lock-free with atomics
var total int64
atomic.AddInt64(&total, 1)Follow-up Questions
- What overhead does the -race detector add and why?
- When would you use sync/atomic instead of a mutex?
- How does 'share memory by communicating' avoid races?
- What is the difference between sync.Mutex and sync.RWMutex?
- Can the race detector produce false positives?
MCQ Practice
1. Which flag enables Go's race detector?
The -race flag (e.g. go test -race) instruments memory accesses to report data races at runtime.
2. A data race requires that concurrent accesses include at least:
A race needs concurrent access to the same memory where at least one access is a write, without synchronization.
3. Which is the cheapest safe way to increment a shared counter?
For simple counters, atomic operations avoid lock overhead while remaining safe under concurrency.
Flash Cards
How do you detect races in Go? — Run with the -race flag, e.g. go test -race or go run -race.
What defines a data race? — Concurrent access to shared memory with at least one write and no synchronization.
Ways to avoid races in Go? — Channels (transfer ownership), sync.Mutex/RWMutex, or sync/atomic.
Does -race catch untriggered races? — No; the racy code path must actually execute concurrently to be detected.
Continue Learning
Related Interview Questions
What are channels in Go and how do they enable communication between goroutines?
medium
What is a Mutex in Go and how does it differ from a channel for synchronization?
medium
What is the difference between buffered and unbuffered channels in Go?
medium
What is a map in Go and is it safe for concurrent use?
medium