Skip to main content
โšก Calmops

Race Conditions and Data Races

Race Conditions and Data Races

Race conditions occur when multiple goroutines access shared data concurrently without proper synchronization. Go provides excellent tools to detect and prevent these bugs. This guide covers race conditions, detection, and prevention strategies.

Understanding Race Conditions

What is a Race Condition?

A race condition occurs when:

  1. Multiple goroutines access the same variable
  2. At least one goroutine modifies the variable
  3. There’s no synchronization between accesses
package main

import (
    "fmt"
    "sync"
)

var counter = 0

func main() {
    var wg sync.WaitGroup
    
    // Start 100 goroutines
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++  // Race condition!
        }()
    }
    
    wg.Wait()
    fmt.Println("Counter:", counter)  // Expected: 100, Actual: varies
}

// Run with: go run -race main.go
// Output will show race condition detected

Why Race Conditions Happen

package main

import "fmt"

// counter++ is not atomic
// It's actually three operations:
// 1. Load counter from memory
// 2. Increment the value
// 3. Store back to memory

// With concurrent access:
// Goroutine 1: Load (0) -> Increment (1) -> Store (1)
// Goroutine 2: Load (0) -> Increment (1) -> Store (1)
// Result: counter = 1 (should be 2)

func main() {
    fmt.Println("Race conditions occur due to non-atomic operations")
}

Detecting Race Conditions

Using the Race Detector

# Run tests with race detector
go test -race

# Run program with race detector
go run -race main.go

# Build with race detector
go build -race

# Run benchmarks with race detector
go test -race -bench=.

Example: Detecting a Race

package main

import (
    "fmt"
    "sync"
)

var counter = 0

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++
        }()
    }
    
    wg.Wait()
    fmt.Println("Counter:", counter)
}

// Run with: go run -race main.go
// Output:
// ==================
// WARNING: DATA RACE
// Write at 0x00c0001b2000 by goroutine 7:
//   main.main.func1()
//       /path/to/main.go:15 +0x44
//
// Previous write at 0x00c0001b2000 by goroutine 6:
//   main.main.func1()
//       /path/to/main.go:15 +0x44
// ==================

Preventing Race Conditions

Using Mutexes

package main

import (
    "fmt"
    "sync"
)

var (
    counter = 0
    mu      sync.Mutex
)

func main() {
    var wg sync.WaitGroup
    
    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)  // Always 100
}

Using Channels

package main

import (
    "fmt"
    "sync"
)

func main() {
    ch := make(chan int)
    var wg sync.WaitGroup
    
    // Sender
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            ch <- 1
        }()
    }
    
    // Receiver
    go func() {
        counter := 0
        for val := range ch {
            counter += val
        }
        fmt.Println("Counter:", counter)
    }()
    
    wg.Wait()
    close(ch)
}

Using Atomic Operations

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }
    
    wg.Wait()
    fmt.Println("Counter:", atomic.LoadInt64(&counter))  // Always 100
}

Common Race Condition Patterns

Pattern 1: Unprotected Shared Variable

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Name string
}

var user *User  // โŒ Race condition

func main() {
    var wg sync.WaitGroup
    
    // Writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        user = &User{Name: "Alice"}
    }()
    
    // Reader
    wg.Add(1)
    go func() {
        defer wg.Done()
        if user != nil {
            fmt.Println(user.Name)
        }
    }()
    
    wg.Wait()
}

// Run with: go run -race main.go
// Will detect race condition

Fix: Use Mutex

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Name string
}

var (
    user *User
    mu   sync.Mutex
)

func main() {
    var wg sync.WaitGroup
    
    // Writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        user = &User{Name: "Alice"}
        mu.Unlock()
    }()
    
    // Reader
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        if user != nil {
            fmt.Println(user.Name)
        }
        mu.Unlock()
    }()
    
    wg.Wait()
}

Pattern 2: Slice Modification

package main

import (
    "fmt"
    "sync"
)

var items []int  // โŒ Race condition

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            items = append(items, val)
        }(i)
    }
    
    wg.Wait()
    fmt.Println("Items:", items)
}

// Run with: go run -race main.go
// Will detect race condition

Fix: Use Mutex

package main

import (
    "fmt"
    "sync"
)

var (
    items []int
    mu    sync.Mutex
)

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            mu.Lock()
            items = append(items, val)
            mu.Unlock()
        }(i)
    }
    
    wg.Wait()
    fmt.Println("Items:", items)
}

Pattern 3: Map Access

package main

import (
    "fmt"
    "sync"
)

var cache = make(map[string]string)  // โŒ Race condition

func main() {
    var wg sync.WaitGroup
    
    // Writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        cache["key"] = "value"
    }()
    
    // Reader
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println(cache["key"])
    }()
    
    wg.Wait()
}

// Run with: go run -race main.go
// Will detect race condition

Fix: Use Mutex

package main

import (
    "fmt"
    "sync"
)

var (
    cache = make(map[string]string)
    mu    sync.Mutex
)

func main() {
    var wg sync.WaitGroup
    
    // Writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        cache["key"] = "value"
        mu.Unlock()
    }()
    
    // Reader
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        fmt.Println(cache["key"])
        mu.Unlock()
    }()
    
    wg.Wait()
}

Synchronization Primitives

Mutex

package main

import (
    "sync"
)

var (
    data = 0
    mu   sync.Mutex
)

func main() {
    mu.Lock()
    data = 42
    mu.Unlock()
}

RWMutex (Read-Write Mutex)

package main

import (
    "sync"
)

var (
    data = 0
    mu   sync.RWMutex
)

func read() int {
    mu.RLock()
    defer mu.RUnlock()
    return data
}

func write(val int) {
    mu.Lock()
    defer mu.Unlock()
    data = val
}

Atomic Operations

package main

import (
    "sync/atomic"
)

var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

func get() int64 {
    return atomic.LoadInt64(&counter)
}

Best Practices

โœ… Good Practices

  1. Always use race detector - go test -race
  2. Protect shared data - Use mutexes or channels
  3. Use channels for communication - Preferred in Go
  4. Keep critical sections small - Minimize lock time
  5. Use RWMutex for read-heavy workloads - Better performance
  6. Use atomic operations for simple counters - Faster than mutexes
  7. Document synchronization - Explain why locks are needed
  8. Test with race detector - Catch bugs early

โŒ Anti-Patterns

// โŒ Bad: Unprotected shared variable
var counter = 0
go func() { counter++ }()
go func() { counter++ }()

// โœ… Good: Protected with mutex
var (
    counter = 0
    mu      sync.Mutex
)
go func() {
    mu.Lock()
    counter++
    mu.Unlock()
}()

// โŒ Bad: Holding lock too long
mu.Lock()
doExpensiveOperation()
mu.Unlock()

// โœ… Good: Minimize critical section
mu.Lock()
data = value
mu.Unlock()
doExpensiveOperation()

// โŒ Bad: Not using race detector
go test

// โœ… Good: Always use race detector
go test -race

Resources and References

Official Documentation

Tools and Resources

Summary

Race conditions are serious bugs:

  • Detect with race detector: go test -race
  • Protect shared data with mutexes
  • Use channels for communication
  • Keep critical sections small
  • Use RWMutex for read-heavy workloads
  • Use atomic operations for simple counters
  • Always test with race detector

Master synchronization for correct concurrent Go programs.

Comments