Explain sync.Mutex and sync.RWMutex — when do you choose a mutex over a channel?
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
| Situation | Prefer |
|---|---|
| Multiple goroutines read/write the same in-memory state (counter, cache, map) | Mutex |
| Handing ownership of a piece of data from one goroutine to another | Channel |
| Signaling "this event happened" or coordinating a pipeline | Channel |
| Protecting a small, hot piece of state where channel overhead would be wasteful | Mutex |
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.