Understanding and Preventing Race Conditions in Go
Race conditions occur when multiple goroutines access shared resources without proper synchronization. This guide covers everything you need to know about detecting and preventing race conditions in Go.
Understanding Race Conditions
What is a Race Condition?
package main
import (
"fmt"
"sync"
)
func main() {
// Example of a race condition
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // Race condition: unsynchronized access
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // Result is unpredictable
}
Types of Race Conditions
Data Race
- Multiple goroutines accessing same memory location
- At least one goroutine is writing
- No synchronization between accesses
Check-Then-Act Race
if value != nil { value.Method() // Race: value could be nil here }
Read-Modify-Write Race
x = x + 1 // Race: read and write not atomic
Detecting Race Conditions
Using the Race Detector
# Run with race detection
go run -race main.go
# Test with race detection
go test -race ./...
# Build with race detection
go build -race
Example Race Detection Output
package main
import (
"fmt"
"sync"
)
func main() {
data := make(map[string]string)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
data["key"] = "value1" // Race: unsynchronized map access
}()
go func() {
defer wg.Done()
fmt.Println(data["key"]) // Race: concurrent read
}()
wg.Wait()
}
Preventing Race Conditions
1. Using Mutexes
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
2. Using Channels
type Counter struct {
ch chan int
value int
}
func NewCounter() *Counter {
c := &Counter{
ch: make(chan int),
}
go func() {
for {
c.value += <-c.ch
}
}()
return c
}
func (c *Counter) Increment() {
c.ch <- 1
}
3. Using sync/atomic
import "sync/atomic"
type AtomicCounter struct {
value atomic.Int64
}
func (c *AtomicCounter) Increment() {
c.value.Add(1)
}
func (c *AtomicCounter) Value() int64 {
return c.value.Load()
}
Best Practices
1. Prefer Channels for Communication
func worker(jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- process(job)
}
}
// Usage
jobs := make(chan int, 100)
results := make(chan int, 100)
go worker(jobs, results)
2. Use sync.Once for One-time Initialization
type Config struct {
once sync.Once
data *Data
}
func (c *Config) LoadData() *Data {
c.once.Do(func() {
c.data = loadDataFromDB()
})
return c.data
}
3. Implement Clear Ownership
type DataStore struct {
mu sync.RWMutex
data map[string]interface{}
}
func (ds *DataStore) Get(key string) interface{} {
ds.mu.RLock()
defer ds.mu.RUnlock()
return ds.data[key]
}
func (ds *DataStore) Set(key string, value interface{}) {
ds.mu.Lock()
defer ds.mu.Unlock()
ds.data[key] = value
}
Common Race Conditions
1. Map Access
// Wrong: Concurrent map access
func wrong() {
m := make(map[string]string)
go func() {
m["key"] = "value"
}()
go func() {
fmt.Println(m["key"])
}()
}
// Right: Use sync.Map or mutex
var m sync.Map
go func() {
m.Store("key", "value")
}()
go func() {
value, _ := m.Load("key")
fmt.Println(value)
}()
2. Slice Access
// Wrong: Concurrent slice access
func wrong() {
s := make([]int, 0)
go func() {
s = append(s, 1)
}()
go func() {
s = append(s, 2)
}()
}
// Right: Use channels or mutex
type SafeSlice struct {
sync.Mutex
data []int
}
func (s *SafeSlice) Append(value int) {
s.Lock()
defer s.Unlock()
s.data = append(s.data, value)
}
3. Lazy Initialization
// Wrong: Race in lazy initialization
var instance *Singleton
func GetInstance() *Singleton {
if instance == nil {
instance = &Singleton{}
}
return instance
}
// Right: Use sync.Once
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
Testing for Race Conditions
1. Write Race-Sensitive Tests
func TestConcurrentAccess(t *testing.T) {
counter := NewSafeCounter()
var wg sync.WaitGroup
// Launch multiple goroutines
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
if counter.Value() != 1000 {
t.Errorf("Expected 1000, got %d", counter.Value())
}
}
2. Stress Testing
func TestStressCounter(t *testing.T) {
counter := NewSafeCounter()
done := make(chan bool)
// Multiple writers
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 1000; j++ {
counter.Increment()
}
done <- true
}()
}
// Wait for all writers
for i := 0; i < 10; i++ {
<-done
}
if counter.Value() != 10000 {
t.Errorf("Race condition detected")
}
}
Next Steps
- Learn about mutexes
- Explore atomic operations
- Study channels
- Practice with worker pools