liveBook · Manning

Data race

As explained in 100 Go Mistakes and How to Avoid Them

A data race is a concurrency error in which multiple executions access the same memory concurrently, at least one access is a write, and the accesses are not properly ordered or synchronized.

A data race occurs when two or more goroutines access the same memory location concurrently and at least one writes to it. A shared variable is not safe merely because each goroutine performs a simple-looking operation. section 8.4.1

Two increments, one lost

Start with `i := 0` and run `i++` in each of two goroutines. Each increment reads `i`, adds one, then writes it back. If both read 0 before either writes, both write 1; if their increments complete in sequence, the result is 2. Running the example with `-race` reports a data race. section 8.4.1

Choose a fix that fits who needs to access the value:

  • Use `atomic.AddInt64(&i, 1)` when the shared operation is an atomic increment. section 8.4.1
  • Put every access that needs protection inside the same mutex-protected critical section. section 8.4.1 section 9.10
  • Have goroutines send values on a channel and let one goroutine update the total. section 8.4.1

A lock only helps if it protects the actual shared data for the entire access. Copying a map variable under a read lock does not copy its data buckets: iterating over that map after unlocking can race with a writer. Keep the lock through a lightweight iteration, or copy the map's entries while locked and process the copy afterward. section 9.10

func (c *Cache) AverageBalance() float64 {
    c.mu.RLock()
    balances := c.balances                  #1
    c.mu.RUnlock()
 
    sum := 0.
    for _, balance := range balances {      #2
        sum += balance
    }
    return sum / float64(len(balances))
}

The read lock ends before iteration, but `balances` still refers to the shared map data; a concurrent `AddBalance` can write while the loop reads. section 9.10

The same aliasing question matters for slices. Concurrent `append` calls on a full slice use new backing arrays in the shown example; with spare capacity, both can write the same backing-array location. Give each goroutine its own copy when it must append independently. Also avoid copying a `sync.Mutex`: a value-receiver method can copy the mutex while retaining access to the same map, so the calls no longer share a critical section. Use a pointer receiver or a properly initialized pointer to the mutex; `go vet` can flag a lock passed by value. section 9.9 section 9.14

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Works through interleaved reads and writes and practical fixes, without surveying every synchronization option.
Examples
Uses competing increments, concurrent slice appends, map iteration after unlocking, and a copied mutex.
What it assumes
Assumes familiarity with goroutines and basic Go variables while explaining why an increment is not one indivisible operation.
What you can do after
Diagnose/debug with `-race`, then protect shared access or give goroutines independent data.
How it connects
Connects data races to goroutines, atomic operations, mutexes, channels, slice backing arrays, and map aliasing.

Also explained in

Compare how 4 books explain data race →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.