Go Mutex & Locks
Protecting shared data from race conditions
🔒 What are Mutex & Locks?
Mutex (mutual exclusion) prevents race conditions by ensuring only one goroutine can access shared data at a time. Locks provide thread-safe access to critical sections of code.
// Basic mutex example
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Printf("Final counter: %d\n", counter)
}
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
Types of Locks
Mutex
Basic mutual exclusion lock
var mu sync.Mutex
mu.Lock()
// Critical section
mu.Unlock()
RWMutex
Reader-writer lock for better performance
var rwmu sync.RWMutex
rwmu.RLock() // Read lock
rwmu.RUnlock()
rwmu.Lock() // Write lock
Once
Execute function only once
var once sync.Once
once.Do(func() {
// Runs only once
})
Atomic
Lock-free atomic operations
import "sync/atomic"
var counter int64
atomic.AddInt64(&counter, 1)
🔹 Race Condition Problem
Without proper synchronization, concurrent access causes race conditions:
package main
import (
"fmt"
"sync"
)
var counter int // Shared variable
func main() {
var wg sync.WaitGroup
// Start 100 goroutines
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Race condition: multiple goroutines modify counter
for j := 0; j < 1000; j++ {
counter++ // NOT thread-safe
}
}()
}
wg.Wait()
fmt.Printf("Expected: 100000, Got: %d\n", counter)
}
Output (varies each run):
Expected: 100000, Got: 87432
🔹 Mutex Solution
Use mutex to protect shared data and eliminate race conditions:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex // Mutex to protect counter
)
func main() {
var wg sync.WaitGroup
// Start 100 goroutines
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
mu.Lock() // Acquire lock
counter++ // Critical section
mu.Unlock() // Release lock
}
}()
}
wg.Wait()
fmt.Printf("Expected: 100000, Got: %d\n", counter)
}
Output (consistent):
Expected: 100000, Got: 100000
🔹 RWMutex for Better Performance
Use RWMutex when you have many readers and few writers:
package main
import (
"fmt"
"sync"
"time"
)
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{
data: make(map[string]int),
}
}
func (sm *SafeMap) Read(key string) int {
sm.mu.RLock() // Multiple readers allowed
defer sm.mu.RUnlock()
return sm.data[key]
}
func (sm *SafeMap) Write(key string, value int) {
sm.mu.Lock() // Exclusive write access
defer sm.mu.Unlock()
sm.data[key] = value
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
// Writer goroutine
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
sm.Write(fmt.Sprintf("key%d", i), i*10)
time.Sleep(100 * time.Millisecond)
}
}()
// Multiple reader goroutines
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
value := sm.Read("key1")
fmt.Printf("Reader %d: %d\n", id, value)
time.Sleep(50 * time.Millisecond)
}
}(i)
}
wg.Wait()
}
🔹 Atomic Operations
For simple operations, use atomic package for better performance:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
// Start 100 goroutines
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
atomic.AddInt64(&counter, 1) // Atomic increment
}
}()
}
wg.Wait()
// Atomic read
final := atomic.LoadInt64(&counter)
fmt.Printf("Final counter: %d\n", final)
// Compare and swap example
old := atomic.LoadInt64(&counter)
swapped := atomic.CompareAndSwapInt64(&counter, old, 0)
fmt.Printf("Swapped: %t, New value: %d\n", swapped, atomic.LoadInt64(&counter))
}
Output:
Final counter: 100000 Swapped: true, New value: 0
🔹 Best Practices
Follow these guidelines for safe concurrent programming:
✅ Do:
- Use defer: Always unlock with defer
- Keep critical sections small: Minimize lock duration
- Use RWMutex: For read-heavy workloads
- Use atomic: For simple numeric operations
- Prefer channels: For communication over shared memory
❌ Don't:
- Forget to unlock: Always pair Lock() with Unlock()
- Lock unnecessarily: Don't over-synchronize
- Create deadlocks: Always acquire locks in same order
- Copy mutexes: Pass by pointer, not value
// Good: Proper mutex usage
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // Always use defer
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
// Bad: Copying mutex
func badExample(c Counter) { // Don't copy mutex!
c.Increment()
}