Skip to content

Go Mutexes — Protecting Shared State with sync.Mutex and sync.RWMutex

DodaTech Updated 2026-06-28 5 min read

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" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go