Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 26

    Mutex

    sync.Mutex provides mutual exclusion — only one goroutine holds the lock at a time.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    sync.Mutex provides mutual exclusion — only one goroutine holds the lock at a time. RWMutex allows multiple readers or one writer. Use mutexes when goroutines share mutable state: counters, caches, connection pools metadata.

    The race detector (go test -race) finds unsynchronized access — run it in CI. Prefer channels for ownership transfer; use mutexes for protecting shared data structures. sync/atomic offers lock-free counters for simple cases.

    Deadlocks from lock ordering and forgotten Unlock are common production bugs — defer Unlock immediately after Lock.

    The story

    An API gateway maintains an in-memory rate-limit map shared across thousands of goroutines handling concurrent requests. A sync.Mutex protects map writes while sync.RWMutex allows parallel reads during traffic peaks — the same pattern in Cloudflare's edge rate limiters where read-heavy workloads dominate.

    Data races corrupt maps silently; run go test -race in CI to catch missing locks before they become production incidents.

    Understanding the topic

    Key concepts

    • mu.Lock() / mu.Unlock() — exclusive access.
    • RWMutex: RLock/RUnlock for readers; Lock/Unlock for writers.
    • defer mu.Unlock() immediately after Lock — panic-safe.
    • sync/atomic for simple counters without full mutex.
    • Don't copy Mutex — contains internal state; use pointer.
    • Race detector flags unsynchronized concurrent access.

    Step-by-step explanation

    1. Goroutine Lock blocks until mutex available.
    2. Unlock releases; next waiting goroutine proceeds.
    3. RLock allows concurrent readers; Lock blocks all.
    4. Lock while holding Lock same mutex — deadlock.
    5. Zero value Mutex ready to use — no initialization needed.
    6. atomic.AddInt64 for hot-path counters.

    Practical code example

    Thread-safe cache with RWMutex:

    go
    package main
    import (
    "fmt"
    "sync"
    )
    type SafeCache struct {
    mu sync.RWMutex
    items map[string]string
    }
    func NewSafeCache() *SafeCache {
    return &SafeCache{items: make(map[string]string)}
    }
    func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.items[key]
    return val, ok
    }
    func (c *SafeCache) Set(key, val string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = val
    }
    func main() {
    cache := NewSafeCache()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(n int) {
    defer wg.Done()
    key := fmt.Sprintf("k%d", n%3)
    cache.Set(key, fmt.Sprintf("v%d", n))
    cache.Get(key)
    }(i)
    }
    wg.Wait()
    fmt.Println("cache safe after concurrent access")
    }

    Line-by-line code explanation

    • var mu sync.Mutex declares a mutual exclusion lock.
    • mu.Lock() acquires the lock — blocks if another goroutine holds it.
    • defer mu.Unlock() releases the lock when the function returns, even on panic.
    • limits[key]++ safe map mutation only while the mutex is held.
    • sync.RWMutexRLock/RUnlock for concurrent readers, Lock for exclusive writers.
    • sync.Map — specialized concurrent map for read-heavy caches (use when profiled).
    • go test -race ./... detects unsynchronized access in CI pipelines.
    • hold locks briefly — never perform I/O or network calls while holding a mutex.

    Key takeaway: RLock for read-heavy Get; Lock for write Set. defer Unlock prevents leak on panic. Never copy SafeCache struct containing Mutex.

    Real-world use

    Where you'll use this in production

    • In-memory rate limiter and session store.
    • Metrics counter aggregation from many goroutines.
    • Lazy initialization of expensive singleton resources.
    • Protecting map writes in concurrent cache.

    Best practices

    • defer mu.Unlock() immediately after Lock.
    • Hold lock for minimal critical section — no I/O inside lock.
    • Consistent lock ordering across goroutines prevents deadlock.
    • Use RWMutex when reads dominate writes.
    • Run go test -race in CI on every PR.
    • Consider sync.Map for read-heavy cache instead of mutex+map.

    Common mistakes

    • Forgotten Unlock — deadlock.
    • Locking during network call — throughput collapse.
    • Copying struct with embedded Mutex.
    • RWMutex: writer starvation under heavy read load — monitor.
    • Using mutex when channel ownership transfer is clearer.

    Advanced interview questions

    Q1BeginnerMutex vs channel?
    Mutex protects shared state; channel transfers ownership — share memory by communicating.
    Q2BeginnerRWMutex when?
    Many concurrent readers, infrequent writers — allows parallel reads.
    Q3IntermediateWhy defer Unlock?
    Ensures release on panic or early return — prevents deadlock.
    Q4IntermediateDetect data race?
    go test -race; TSAN in CI; review shared mutable state access.
    Q5AdvancedDesign concurrent cache with TTL eviction.
    RWMutex + map; background goroutine sweeps; atomic stats; or use ristretto with built-in concurrency.

    Summary

    Mutex serializes access to shared mutable state. RWMutex optimizes read-heavy workloads. Always defer Unlock; keep critical sections small. Run race detector in CI — catches unsynchronized access. Next lesson: context — cancellation and deadlines.

    Ready to mark this lesson complete?Track your journey across the entire course.