Explain sync.Mutex and sync.RWMutex — when do you choose a mutex over a channel?

5 minintermediategomutexsyncconcurrency

Quick Answer

sync.Mutex provides mutual exclusion: mu.Lock() blocks until the lock is free, and mu.Unlock() releases it, guarding a critical section so only one goroutine touches shared state at a time. sync.RWMutex adds a distinction between Lock()/Unlock() (exclusive, for writers) and RLock()/RUnlock() (shared, for readers) — multiple readers can hold the read lock simultaneously, which helps when reads vastly outnumber writes. Go's own guidance ("share memory by communicating") favors channels for handing off ownership of data or coordinating goroutines, but a plain mutex is usually simpler and faster for protecting a small piece of shared state, like a counter or a cache map, that many goroutines read and write directly. Use a mutex when goroutines share state; use a channel when goroutines hand off data or signal each other.

Detailed Answer

Go gives you two legitimate concurrency tools, mutexes and channels, and picking the right one for the job matters.

Basic mutex usage

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

defer c.mu.Unlock() right after Lock() is the idiomatic pattern — it guarantees the lock releases even if the function panics or has multiple return paths.

RWMutex for read-heavy workloads

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

Many goroutines can call Get concurrently, since RLock doesn't exclude other readers. Set still needs the exclusive Lock, since a write must not race with any read or another write.

Mutex vs. channel: a rule of thumb

SituationPrefer
Multiple goroutines read/write the same in-memory state (counter, cache, map)Mutex
Handing ownership of a piece of data from one goroutine to anotherChannel
Signaling "this event happened" or coordinating a pipelineChannel
Protecting a small, hot piece of state where channel overhead would be wastefulMutex

Go's proverb "share memory by communicating" is a design philosophy, not a hard rule. Real Go codebases use both, choosing whichever tool more directly expresses the actual problem.

Related Resources