Is sync.Mutex reentrant? What happens with a recursive RLock on an RWMutex?
Question 223HardGo 1.22 to 1.25
No. Go mutexes are not reentrant and are not tied to a goroutine. If the same goroutine calls Lock twice, it deadlocks. One goroutine may also Unlock a mutex that a different goroutine locked; this is legal but rarely a good idea.
A recursive RLock is the subtler trap. It works until a writer shows up:
func (s *Store) Total() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.sumLocked() + s.Count() // Count() calls RLock again
}
func (s *Store) Count() int {
s.mu.RLock() // blocks if a writer called Lock in between
defer s.mu.RUnlock()
return len(s.items)
}
The sequence that deadlocks:
- Goroutine A holds a read lock.
- Goroutine B calls
Lockand waits for readers to drain. - A calls
RLockagain. That call blocks because a writer is pending. - B waits on A and A waits on B.
It passes tests and then deadlocks under production load. The Go docs forbid recursive read locking explicitly.
Fix: split the code into exported methods that take the lock and unexported xxxLocked() helpers that assume the lock is already held.
More on Concurrency Patterns & sync
- Q221When would you use sync.RWMutex over sync.Mutex, and when can RWMutex actually be slower?
- Q222Explain sync.Mutex "normal mode" vs "starvation mode".
- Q224What does this print, and why? (copying a lock)
- Q225What is the classic sync.WaitGroup bug, and what does wg.Go (Go 1.25) change?
- Q226Explain sync.Once semantics, including panics, and the OnceFunc / OnceValue / OnceValues helpers.
- Q227When would you use sync.Cond, and why must Wait always be called in a loop?