Data Race vs Race Condition in Go: Clear Differences and How to Fix Them

The article explains the distinction between a data race—simultaneous unsynchronized memory access by goroutines—and a race condition—logic errors caused by timing dependencies—using Go code examples, demonstrates how to reproduce each issue, and shows how mutexes or atomic operations can resolve them.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
Data Race vs Race Condition in Go: Clear Differences and How to Fix Them

Data Race

A data race occurs when multiple goroutine(s) access the same memory location concurrently and at least one of the accesses is a write.

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup
    // Launch 100 goroutine(s)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // Data race: concurrent read‑write on counter
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // Result nondeterministic, may be wrong
}

// Solution: use a mutex or atomic operation
func main() {
    counter := 0
    var wg sync.WaitGroup
    var mu sync.Mutex
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // Correct result: 100
}

Race Condition

A race condition is a higher‑level bug where program correctness depends on the relative timing of multiple threads; it can exist even without a data race.

package main

import (
    "fmt"
    "sync"
    "time"
)

type Account struct {
    balance int
    mu      sync.Mutex
}

func main() {
    account := &Account{balance: 100} // initial balance 100
    var wg sync.WaitGroup
    // Even with a mutex protecting the balance, the check‑then‑act logic is not atomic
    wg.Add(2)
    go func() {
        defer wg.Done()
        // First withdrawal
        if account.balance >= 100 { // check
            time.Sleep(time.Millisecond) // simulate processing time
            account.mu.Lock()
            account.balance -= 100 // withdraw 100
            account.mu.Unlock()
        }
    }()
    go func() {
        defer wg.Done()
        // Second withdrawal
        if account.balance >= 100 { // check
            time.Sleep(time.Millisecond) // simulate processing time
            account.mu.Lock()
            account.balance -= 100 // withdraw 100
            account.mu.Unlock()
        }
    }()
    wg.Wait()
    fmt.Println("Final balance:", account.balance)
}

Main Differences

Data Race :

Low‑level concept focusing on memory access.

Occurs when multiple goroutine(s) simultaneously access the same memory location, with at least one write.

Can be avoided with mutexes or atomic operations.

Go provides the -race detector to find data races.

Always indicates a bug in the program.

Race Condition :

Higher‑level concept focusing on program logic.

Program correctness depends on the ordering of events.

Can exist even without a data race.

Harder to detect and fix.

Often stems from design issues.

In short, a data race concerns concurrent reads/writes to the same memory location, while a race condition concerns the program’s behavior depending on the relative order of events.

concurrencyGomutexGoroutinerace conditionatomicdata race
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.