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()
}

🧠 Test Your Knowledge

What does mutex stand for?