Data Races and Mutexes

Updated

September 8, 2026

Data Races and Mutexes

Overview

When multiple goroutines access the same variable and at least one writes, you have a data race unless synchronization creates happens-before. Maps can fatal on concurrent write; other races may silently corrupt.

Concurrent map writes

counter := map[string]int{}
// two goroutines: counter[word]++  → often:
// fatal error: concurrent map writes

counter[word]++ is not one atomic step (hash, read, add, write, maybe grow).

Race detector

go run -race .
go test -race ./...
WARNING: DATA RACE
Read at ... by goroutine 6
Previous write at ... by goroutine 7

Races are timing-dependent—passing once means nothing. Always CI with -race on supported platforms (needs CGO toolchain historically).

Channels are race-free for their send/receive protocol; sharing the payload pointers can still race.

Fix 1: avoid shared mutation

each worker → private map
main → merge maps after Wait
// counters[i] written only by worker i — OK for distinct indices
// do not append to shared slice from many Gs without sync

Or send partial maps on a channel and merge in one owner goroutine (share by communicating).

Fix 2: mutex

var mu sync.Mutex
mu.Lock()
counter[word]++
mu.Unlock()
sequence (top → bottom):
  actors: G1, Mutex, G2
  G1 --> Mutex  : Lock
  G2 --> Mutex  : Lock (blocks)
  G1 --> G1  : critical section
  G1 --> Mutex  : Unlock
  Mutex --> G2  : acquired
  G2 --> G2  : critical section
  G2 --> Mutex  : Unlock

Use mutex when:

  • Multiple writers, or
  • Writer + readers without other sync

Readers-only → no mutex needed for that data.

Non-reentrant

mu.Lock()
mu.Lock() // deadlock — Go mutex is not reentrant

Pass by pointer

Same rule as WaitGroup—copies break exclusion.

RWMutex

var rw sync.RWMutex
// readers
rw.RLock(); _ = m[k]; rw.RUnlock()
// writer
rw.Lock(); m[k]=v; rw.Unlock()

Many concurrent readers; writers exclusive. Measure—RWMutex is not always faster.

Writer W    Readers R1 R2 R3 R4

Mutex:      W excludes everyone; each R serializes
RWMutex:    R1–R4 may overlap; W waits for readers

Critical section hygiene

Do Don’t
Lock only around shared data Hold lock across HTTP/DB
Unlock via defer when multi-return Forget unlock on error path
Document what mu protects One global mutex for everything

Runnable example

go run -race .
package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    counter := map[string]int{}
    var wg sync.WaitGroup
    inc := func(word string) {
        defer wg.Done()
        mu.Lock()
        counter[word]++
        mu.Unlock()
    }
    wg.Add(2)
    go inc("go")
    go inc("go")
    wg.Wait()
    fmt.Println(counter)
}

What to notice: -race is clean; remove mutex and it may fail or warn.

Try next: Rewrite with per-worker maps + merge; compare to mutex under -bench.