Mutex
sync.Mutex provides mutual exclusion — only one goroutine holds the lock at a time.
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
- Goroutine Lock blocks until mutex available.
- Unlock releases; next waiting goroutine proceeds.
- RLock allows concurrent readers; Lock blocks all.
- Lock while holding Lock same mutex — deadlock.
- Zero value Mutex ready to use — no initialization needed.
- atomic.AddInt64 for hot-path counters.
Practical code example
Thread-safe cache with RWMutex:
package mainimport ("fmt""sync")type SafeCache struct {mu sync.RWMutexitems 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.WaitGroupfor 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.Mutexdeclares 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.RWMutex—RLock/RUnlockfor concurrent readers,Lockfor 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?
Q2BeginnerRWMutex when?
Q3IntermediateWhy defer Unlock?
Q4IntermediateDetect data race?
Q5AdvancedDesign concurrent cache with TTL eviction.
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.