skip to content

In Go's sync.RWMutex, what is the difference between RLock and Lock?

level: juniorimportance: must knowfreq 72%

answer

  1. two modes, not one
  2. many at once, or one alone
  3. readers share; a writer needs the room empty
  4. the R-prefixed pair takes the shared mode

basics

~20 s

RLock takes a shared read lock: any number of goroutines can hold it at once, but only while no writer holds the lock. Lock takes an exclusive write lock, so it waits until every reader and every other writer has released.

solid answer

~50 s

`sync.RWMutex` has two acquisition modes. `RLock`/`RUnlock` take the shared mode: many goroutines can read the guarded data at the same time, and they are blocked only while a writer holds the lock or is waiting for it. `Lock`/`Unlock` take the exclusive mode: one writer inside, no readers. You pair the calls exactly — a section entered with `RLock` leaves with `RUnlock`, normally `defer mu.RUnlock()` — and calling `Unlock` on a lock that was taken for reading is a fatal runtime error, not a recoverable panic. The point of the type is throughput on read-mostly state: a routing table consulted on every inbound request and rewritten occasionally is the textbook fit. It is not free, though. `RWMutex` carries more bookkeeping than `sync.Mutex`, so when the guarded work is a single map lookup the plain mutex is usually just as fast.

code

go · 17 lines
go
type Router struct {
	mu     sync.RWMutex
	routes map[string]string
}

func (r *Router) Lookup(path string) (string, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	h, ok := r.routes[path]
	return h, ok
}

func (r *Router) Replace(routes map[string]string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.routes = routes
}

go deeper

for a junior

Be ready to name the four calls and say which mode admits many goroutines and which admits one. Show the pairing habit: RLock with defer RUnlock, Lock with defer Unlock.

for a middle

Explain what the read lock excludes rather than reciting the API. Writers wait for the last reader to leave, and the read lock guarantees nothing at all once RUnlock has returned.

for a senior

Expect to justify the choice with evidence. Say when you would keep a plain Mutex because the guarded section is one lookup, and how you would measure the difference before switching a hot path.

for a principal

Own the guidance the team follows: which shared types are documented as safe for concurrent use, and whether a component should hide its locking behind a narrow API rather than exporting a lock other packages must remember to take.

## Two modes instead of one A `sync.Mutex` admits exactly one goroutine at a time, whether it is going to read the guarded data or change it. A `sync.RWMutex` (a readers-writer lock) splits that into two modes: - **Shared, or read, mode** — `mu.RLock()` … `mu.RUnlock()`. Any number of goroutines may hold the read lock simultaneously. Holding it guarantees that no writer is inside the critical section and that none can get in until you release. - **Exclusive, or write, mode** — `mu.Lock()` … `mu.Unlock()`. Exactly one goroutine is inside, and no readers are. The two modes never overlap. Readers exclude writers, writers exclude everyone. What readers do **not** exclude is each other, and that is the entire reason the type exists: if a piece of state is read on every request and rewritten a few times an hour, serialising the reads behind one mutex wastes the machine. ## The contract you have to honour **Pair the calls exactly.** `RLock` with `RUnlock`, `Lock` with `Unlock`. Crossing them is not a compile error and not a recoverable panic — the runtime kills the process with a fatal error such as `sync: Unlock of unlocked RWMutex`, which `recover` cannot catch. Using `defer mu.RUnlock()` on the line after `mu.RLock()` is the habit that makes this a non-issue, including on early returns and on a panic. **The lock is not owned by a goroutine.** The implementation counts readers; it does not record who they are. A different goroutine may legally call `RUnlock` for a read lock another goroutine took. It is legal and almost always a mistake in design terms, because nothing then keeps acquire and release in the same place. **The zero value is usable.** `var mu sync.RWMutex` is an unlocked lock, so an `RWMutex` embedded as the first field of a struct works with no constructor. Like `sync.Mutex`, it must not be copied once it has been used — pass a pointer to the struct that contains it, never the struct by value. **`RLocker`** returns a `sync.Locker` whose `Lock` and `Unlock` methods call `RLock` and `RUnlock`, for code that wants to hand the shared mode to an API that only accepts a `Locker`. ## What the read lock actually promises It promises mutual exclusion against writers **for as long as you hold it**, and nothing after that. This is the part juniors most often get wrong. If you take `RLock`, grab a pointer, a slice or a map out of the guarded struct, `RUnlock`, and then read through what you grabbed, you are reading shared memory with no lock held. Values that carry no interior reference — an `int`, a `bool`, a `string` — are safe to carry out, because copying them really copies them. References are not. ## Why not use it everywhere An `RWMutex` is internally a mutex plus reader counters and semaphores. An uncontended `sync.Mutex` acquisition is roughly one atomic compare-and-swap; `RLock` is at minimum an atomic add on a counter that **every reader on every core touches**, plus the matching subtract in `RUnlock`. That shared counter is a cache line bouncing between cores even when no writer ever appears. So the shared mode buys you concurrency but charges you for coordination, and the trade only pays when: - reads genuinely dominate writes, and - the read critical section is long enough to amortise the bookkeeping — a scan, a decode, several dependent lookups — rather than one map read, and - there are enough cores actually running readers concurrently to have something to gain. Start with `sync.Mutex`, and change only with a measurement in hand. ## The shape in practice A router keeps a `map[string]string` of path to handler name. `Lookup` takes `RLock`, reads, and returns; thousands of requests a second run through it in parallel. `Replace` takes `Lock` and swaps the map. Because the map is replaced rather than edited, the write section is a single assignment and readers stall for almost no time. ## Mistakes to avoid - Believing `RLock` allows concurrent *writes*. It allows concurrent *reads*, and mutating anything under a read lock is a data race. - Assuming `RWMutex` is a free upgrade over `Mutex`. - Doing I/O, logging or an expensive allocation while holding the write lock, which stalls every reader. - Treating a pointer or slice carried out of the read section as private.

  • If no goroutine ever calls Lock, does RLock still cost anything?
    Yes. `RLock` and `RUnlock` are atomic operations on a reader counter that every reader on every core touches, so the cache line holding it bounces between cores even with no writer in sight. For a very short critical section that coordination can cost about what an uncontended `sync.Mutex` costs, which is why the swap sometimes buys nothing.
  • Can one goroutine call RUnlock for a read lock that a different goroutine took with RLock?
    Yes — the implementation counts readers rather than recording which goroutine holds what, so it is legal. It is still a bad idea: nothing keeps acquisition and release together, and a missed release leaks the reader count and blocks every future writer forever. Keep `RLock` and `defer mu.RUnlock()` in the same function.
  • What happens if you call Unlock on a lock you took with RLock?
    The runtime sees an unlock of a lock that is not write-held and terminates the program with a fatal error such as `sync: Unlock of unlocked RWMutex`. It is not an ordinary panic, so `recover` cannot catch it and no deferred cleanup saves you. The same applies to `RUnlock` without a matching `RLock`.

A reading room with one notice board: any number of people can read the notice at the same time, but the person replacing it needs everyone out of the room first.

saying these in an interview costs you the question

  • Says RLock lets several goroutines write at once
  • Claims RWMutex is always faster than a plain Mutex
  • Releases a read lock with Unlock instead of RUnlock
  • Thinks a reader and a writer can be inside together
  • Assumes guarded data stays protected after RUnlock returns