Why does a sync.Mutex need no constructor, and what is the standard Lock and Unlock pattern?
answer
- there is no New function in sync
- declaring it is already enough
- look at the line right after Lock
- covers early returns and panics too
- unexported field, pointer receiver
basics
~20 sA sync.Mutex zero value is already an unlocked, usable lock, so a plain declaration needs no constructor. Idiomatic use is mu.Lock() followed immediately by defer mu.Unlock(), which releases it on every return path, panics included.
solid answer
~50 s`sync.Mutex` is a struct whose zero value is a ready-to-use unlocked mutex, so `var mu sync.Mutex` or a `mu sync.Mutex` field inside a struct is fully initialised the moment it is declared - there is no `NewMutex`, no `make`, nothing to call. The usual shape is an unexported `mu` field placed directly above the fields it guards, with a comment naming them, and methods on a *pointer* receiver that do `s.mu.Lock()` and then `defer s.mu.Unlock()` on the next line. Writing the `defer` immediately after the `Lock` means the mutex is released on early returns and on a panic, and a reviewer can see the pairing at a glance. `Lock` blocks until the mutex is free; it returns nothing and cannot fail. Nothing in the compiler ties the mutex to the data - it guards those fields only because every path agrees to take it.
code
go · 17 linestype Scoreboard struct {
mu sync.Mutex // guards wins and losses
wins int
losses int
}
func (s *Scoreboard) RecordWin() {
s.mu.Lock()
defer s.mu.Unlock()
s.wins++
}
func (s *Scoreboard) Snapshot() (wins, losses int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.wins, s.losses
}go deeper
Be ready to write the four lines from memory: declare the mutex as a struct field, Lock, defer Unlock on the next line, then touch the guarded fields. Say out loud that no constructor is needed.
Explain why defer is the idiom rather than a trailing Unlock - early returns and panics - and why the mutex field is unexported and placed above the data it guards with a comment.
Show the discipline you enforce in review: pointer receivers everywhere on a lock-bearing type, one documented owner per field group, and go test -race in CI to catch the access path that skipped the lock.
Frame it as an API decision: a package that exposes its lock has published a concurrency contract it must keep forever, whereas one that keeps the mutex unexported can change its internal synchronisation later without breaking a single caller.
## What a sync.Mutex is A `sync.Mutex` is Go's basic mutual-exclusion lock: at most one goroutine may hold it at a time. It exists so that several goroutines can touch the same variables without their reads and writes overlapping - which in Go is not merely untidy but a data race, undefined behaviour that the compiler and runtime are free to turn into corrupted values or crashes. The type has exactly two methods you use day to day: `Lock`, which blocks the calling goroutine until the mutex is available and then takes it, and `Unlock`, which releases it. Neither returns anything. ## Why there is no constructor Go's standard library leans hard on the idea of a *usable zero value*: a type whose all-zero form is already meaningful. `sync.Mutex`'s zero value is an unlocked mutex, so all of these are correct and complete: ```go var mu sync.Mutex // package-level or local type Scoreboard struct { mu sync.Mutex // ready as soon as a Scoreboard exists wins int } board := &Scoreboard{} // nothing else to initialise ``` There is deliberately no `sync.NewMutex`. If you ever see one in a codebase, it is somebody's own wrapper. This matters more than it sounds: because the zero value works, a struct that embeds a mutex needs no constructor of its own, and a forgotten initialisation cannot silently produce a lock that does not lock. The other half of that bargain is the rule from the package docs: **a `Mutex` must not be copied after first use.** Because it is a plain struct, assigning it or passing it by value copies it, and the copy is a *different* lock. That is why lock-bearing types are used through pointers and their methods take pointer receivers. ## The Lock / defer Unlock pattern The idiom is two adjacent lines: ```go func (s *Scoreboard) RecordWin() { s.mu.Lock() defer s.mu.Unlock() s.wins++ } ``` Why `defer` rather than a plain `s.mu.Unlock()` at the end? - **Every return path is covered.** A function that grows a second `return` later cannot forget one. - **A panic still releases the lock.** Deferred calls run while the goroutine unwinds, so a panic in the middle of the critical section does not leave the mutex held forever - every other goroutine would otherwise block on it for the life of the process. - **It is reviewable.** `Lock` and `defer Unlock` on consecutive lines is a pattern a reader recognises instantly; a lone `Lock` with the matching `Unlock` forty lines away is where bugs live. ## Where the mutex lives Convention is a small unexported field placed immediately above the data it protects, with a comment that says what it guards: ```go type Scoreboard struct { mu sync.Mutex // guards wins and losses wins int losses int } ``` Keep the field unexported so callers outside the package cannot lock or unlock it; the type's methods are the only thing that should. For the same reason, avoid *embedding* `sync.Mutex` anonymously in an exported struct - embedding promotes `Lock` and `Unlock` into the type's public API, which you then have to support forever and which invites callers to take the lock for you. ## Things worth knowing early - **`Lock` cannot fail and cannot time out.** There is no error return and no deadline parameter. If the mutex is held, your goroutine waits. - **Unlocking an unlocked mutex is fatal.** The program dies with `fatal error: sync: unlock of unlocked mutex`; `recover` does not catch it, because it is a runtime fatal error rather than an ordinary panic. - **A `sync.Mutex` has no owner.** It records nothing about which goroutine locked it, so it is legal (though unusual and hard to review) for one goroutine to lock it and another to unlock it. The absence of ownership is also why locking it twice from the same goroutine deadlocks rather than being detected. - **The mutex protects nothing by itself.** The compiler will not stop you reading `s.wins` without taking `s.mu`; the guarantee comes entirely from every access agreeing to take the lock. That agreement is what the comment above the field is documenting. - **Run the race detector.** `go test -race` is what catches an access path that forgot the lock; a clean build proves nothing about it. ## A minimal mental model Declare it, never copy it, lock it, defer the unlock, touch the guarded fields, return. Everything else about `sync.Mutex` - non-reentrancy, `TryLock`, contention - is a refinement of those five steps.
- Must the goroutine that locked a sync.Mutex be the one that unlocks it?No. A `sync.Mutex` stores no owner, and the package documentation explicitly allows one goroutine to lock it and arrange for another to unlock it. In practice this is rare and hard to review, so almost all code keeps `Lock` and its `defer Unlock` in the same function. The absence of ownership is also why the mutex cannot detect a recursive lock.
- What happens if Unlock is called on a sync.Mutex that is not currently locked?The program dies immediately with `fatal error: sync: unlock of unlocked mutex`. It is a runtime fatal error, not a normal panic, so a deferred `recover` cannot catch it and no other goroutine gets to continue. It usually means an `Unlock` was written on a path that never took the lock, or that the same critical section is being unlocked twice.
- Should you embed sync.Mutex anonymously in an exported struct?Usually not. Embedding promotes `Lock` and `Unlock` onto the type, so they become part of its exported API and any caller can hold your lock for as long as it likes. A named unexported field, `mu sync.Mutex`, keeps the locking discipline inside the package where you can audit it, and lets you rename or split the lock later without breaking callers.
It is a single key hanging on a hook: whoever takes it is the only one in the room, and the deferred Unlock is the rule that you hang the key back before you leave, even if you leave in a hurry.
saying these in an interview costs you the question
- Says you must call something like sync.NewMutex or make first
- Thinks a zero-value sync.Mutex starts out locked
- Locks but unlocks only on the happy path, leaking the lock on error returns
- Assumes the compiler enforces which fields the mutex guards
- Embeds sync.Mutex anonymously and exports Lock and Unlock by accident
- Believes Lock returns an error you should check