Go Mutexes — Protecting Shared State with sync.Mutex and sync.RWMutex
In this tutorial, you will learn about Go Mutexes. We cover key concepts, practical examples, and best practices to help you master this topic.
Go sync.Mutex and sync.RWMutex protect shared state in concurrent programs with Lock/Unlock for exclusive access and RLock/RUnlock for read sharing.
What You'll Learn
- Mutex fundamentals (Lock/Unlock)
- RWMutex for read-optimized locking
- Common patterns and pitfalls
- When to use channels vs mutexes
Why It Matters
Mutexes prevent race conditions. Docker uses mutexes for state protection. Kubernetes uses them for cache synchronization. DodaZIP uses mutexes for shared configuration.
Real-World Use
Configuration Management, cache synchronization, counter/statistics tracking, resource pool management.
flowchart LR
A["Mutexes"] --> B["sync.Mutex"]
A --> C["sync.RWMutex"]
B --> D["Lock/Unlock"]
C --> E["RLock/RUnlock"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Mutex
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var c Counter
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Increment()
}()
}
wg.Wait()
fmt.Println("Counter:", c.Value())
}
RWMutex
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
cache := NewCache()
var wg sync.WaitGroup
// Multiple concurrent readers
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cache.Get("key")
}()
}
// Single writer
wg.Add(1)
go func() {
defer wg.Done()
cache.Set("key", "value")
}()
wg.Wait()
}
Map with Mutex
type SafeMap[K comparable, V any] struct {
mu sync.Mutex
data map[K]V
}
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{data: make(map[K]V)}
}
func (m *SafeMap[K, V]) Set(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
m.mu.Lock()
defer m.mu.Unlock()
val, ok := m.data[key]
return val, ok
}
func (m *SafeMap[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.data, key)
}
Atomic Counter with sync/atomic
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Counter:", atomic.LoadInt64(&counter))
}
Common Mistakes
1. Forgetting to Unlock
mu.Lock()
if condition {
return // Forgot to unlock! Deadlock on next Lock
}
mu.Unlock()
2. Copying Mutex
func worker(m sync.Mutex) { } // Copy! Must use *sync.Mutex
3. Lock Order Deadlock
// Thread 1: Lock A -> Lock B
// Thread 2: Lock B -> Lock A
// Deadlock! Always lock in the same order.
4. Not Protecting All Access
func (c *Counter) Get() int {
return c.value // Not protected! Race condition
}
5. RWMutex Write Starvation
Heavy read load can starve writers. RWMutex favors writers in Go to prevent starvation.
Practice Questions
1. What is the difference between Mutex and RWMutex? Mutex: exclusive lock for all operations. RWMutex: multiple readers or one writer. Readers don't block each other.
2. What is a deadlock? Two goroutines each hold a lock the other needs. They block forever. Fix by consistent lock ordering.
3. When should I use atomic over Mutex? For simple counters and flags. atomic operations are faster but limited to integers and pointers.
4. Can a locked mutex be locked again? No. sync.Mutex is not reentrant. Locking an already-locked mutex deadlocks.
Challenge: Implement a thread-safe queue using a mutex and a slice.
Solution
type SafeQueue[T any] struct {
mu sync.Mutex
items []T
}
func (q *SafeQueue[T]) Enqueue(item T) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
}
func (q *SafeQueue[T]) Dequeue() (T, bool) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.items) == 0 {
var zero T
return zero, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
FAQ
{{< faq question="Should I use channels or mutexes?" >}} Channels for communication (passing data between goroutines). Mutexes for protecting shared state. Prefer channels when possible, mutexes when channels add complexity. {{< /faq >}}
{{< faq question="What happens if I unlock a mutex that isn't locked?" >}} Panic. Always pair Lock with Unlock. Use defer to ensure Unlock runs even with panics. {{< /faq >}}
{{< faq question="Is RWMutex always faster than Mutex?" >}} No. For short critical sections, Mutex is often faster. RWMutex helps when reads significantly outnumber writes and critical sections are long. {{< /faq >}}
{{< faq question="Can I use defer with mutex?" >}}
Yes. defer mu.Unlock() is the idiomatic pattern. Ensures Unlock runs even if the function panics.
{{< /faq >}}
{{< faq question="What is sync.Map?" >}} A concurrent map optimized for specific access patterns: write-once/read-many, or disjoint key sets. Not a general-purpose replacement for map+Mutex. {{< /faq >}}
Try It Yourself
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
n int
}
func (s *SafeCounter) Inc() {
s.mu.Lock()
defer s.mu.Unlock()
s.n++
}
func main() {
var wg sync.WaitGroup
c := SafeCounter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
fmt.Println(c.n)
}
Expected output:
1000
What's Next
Now that you understand mutexes, explore the Context package for cancellation and deadlines.
| Topic | Description | Link |
|---|---|---|
| Go Context | Cancellation and deadlines | {{< ref "22-context" >}} |
| Go WaitGroups | Goroutine synchronization | {{< ref "20-waitgroups" >}} |
| Go Concurrency Patterns | Advanced patterns | {{< ref "24-concurrency-patterns" >}} |