>_
GolangStepByStep
Software Engineer

Race Conditions & -race Debugging

Detect and fix data races with Go's -race detector

# 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.
practice & review