# What a Race Really Is
A race is not a crash. It is incorrectness. The program can appear to work, but it is reading and writing shared memory without ordering. That makes the output non-deterministic.
var count int
for i := 0; i < 1000; i++ {
go func() {
count++
}()
}
fmt.Println(count) // could be 0..1000# Reproduce and Detect
Start by reproducing. Then run with the race detector. It is the fastest way to get concrete stack traces for the conflicting access.
go test -race ./...
// or for a single package
// go test -race ./pkg/store# Fix by Ownership (Channels)
The cleanest fix is to avoid shared state. Give one goroutine ownership and send updates through a channel.
type Counter struct {
in chan int
}
func NewCounter() *Counter {
c := &Counter{in: make(chan int)}
go func() {
total := 0
for delta := range c.in {
total += delta
}
}()
return c
}# Fix by Lock (Mutex)
If state must be shared, protect it with a mutex. Keep the locked section minimal and keep ownership obvious.
var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()# Fix by Atomic (Simple Counters)
Atomics are safe for simple counters and flags, but they are easy to misuse for compound state.
var count int64
atomic.AddInt64(&count, 1)
fmt.Println(atomic.LoadInt64(&count))# Debugging Checklist
- Reproduce with a small test case and run `-race`.
- Identify the shared variable and the conflicting access paths.
- Pick a fix: ownership (channels), mutex, or atomic.
- Add tests that stress the concurrent path.
- Keep the critical section small and obvious.
⚡ Key Takeaways
- Races cause incorrectness, not just crashes.
- `go test -race` is your fastest signal.
- Prefer ownership via channels when possible.
- Use mutexes for compound shared state.
- Use atomics only for simple invariants.