Go Mutex

A Mutex (Mutual Exclusion) prevents multiple goroutines from accessing the same data at the same time. When goroutines read and write shared variables concurrently without protection, the result becomes unpredictable. A Mutex locks access so only one goroutine can use the shared data at any given moment.

The Problem Without a Mutex

package main

import (
    "fmt"
    "sync"
)

var counter int

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    counter++ // multiple goroutines reading and writing simultaneously
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Counter:", counter) // may print something less than 1000
}

Without protection, the final counter value is unpredictable. This is called a data race — two goroutines read and write the same variable at the same moment, and one update overwrites the other.

Data Race Diagram

Without Mutex:

Goroutine A reads counter = 5
Goroutine B reads counter = 5   ← both read same value
Goroutine A writes counter = 6
Goroutine B writes counter = 6  ← overwrites A's update!
                                   one increment is lost

Fixing It with a Mutex

package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()   // lock before accessing shared data
    counter++
    mu.Unlock() // unlock after done
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Counter:", counter) // always 1000
}

Mutex Operation Diagram

With Mutex:

Goroutine A: mu.Lock()  → gets the lock
Goroutine B: mu.Lock()  → BLOCKED, waits

Goroutine A: counter++
Goroutine A: mu.Unlock() → releases the lock

Goroutine B: gets the lock → counter++
Goroutine B: mu.Unlock()

Result: every increment is counted correctly

Using defer with Mutex

Always use defer mu.Unlock() immediately after mu.Lock(). This guarantees the lock is released even if the function panics or returns early.

func safeIncrement() {
    mu.Lock()
    defer mu.Unlock() // always unlocks, no matter what

    counter++
}

RWMutex – Read/Write Mutex

A regular Mutex blocks all access including reads. When many goroutines only read shared data and few write it, sync.RWMutex is more efficient. Multiple readers can hold the lock at the same time; a writer gets exclusive access.

package main

import (
    "fmt"
    "sync"
)

var (
    data  = make(map[string]string)
    rwmu  sync.RWMutex
)

func readData(key string) string {
    rwmu.RLock()         // multiple readers allowed simultaneously
    defer rwmu.RUnlock()
    return data[key]
}

func writeData(key, value string) {
    rwmu.Lock()          // exclusive access for writers
    defer rwmu.Unlock()
    data[key] = value
}

func main() {
    writeData("name", "Alice")
    writeData("city", "Delhi")

    fmt.Println(readData("name")) // Alice
    fmt.Println(readData("city")) // Delhi
}

Mutex vs RWMutex

Featuresync.Mutexsync.RWMutex
Lock methodLock() / Unlock()Lock() / Unlock() for writes
Read lock methodNot availableRLock() / RUnlock() for reads
Concurrent reads?No — one at a timeYes — many readers at once
Best forFrequent writesMany reads, few writes

Detecting Data Races

Go has a built-in race detector. Run a program with -race to find all data races.

go run -race main.go
go test -race ./...

Key Points

  • A data race occurs when two goroutines access shared data concurrently and at least one writes
  • mu.Lock() blocks other goroutines from entering the protected section
  • mu.Unlock() releases the lock — always use defer mu.Unlock() for safety
  • sync.RWMutex allows many concurrent readers but only one writer at a time
  • Run go run -race main.go to detect data races during development

Leave a Comment