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
| Feature | sync.Mutex | sync.RWMutex |
|---|---|---|
| Lock method | Lock() / Unlock() | Lock() / Unlock() for writes |
| Read lock method | Not available | RLock() / RUnlock() for reads |
| Concurrent reads? | No — one at a time | Yes — many readers at once |
| Best for | Frequent writes | Many 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 sectionmu.Unlock()releases the lock — always usedefer mu.Unlock()for safetysync.RWMutexallows many concurrent readers but only one writer at a time- Run
go run -race main.goto detect data races during development
