Why does go vet flag a struct containing a sync.Mutex when it is passed or received by value?
answer
- it is a struct, not a reference
- two copies means two locks
- the copy can inherit a held state
- check the receiver in the diff
- the copylocks analyzer in go vet
basics
~20 sCopying the struct copies the mutex, so each copy has its own independent lock and the copies no longer exclude each other. Go vet's copylocks check reports it as passing a lock by value; the fix is pointer receivers and pointer parameters.
solid answer
~50 sA `sync.Mutex` is an ordinary struct value, not a reference, so any assignment, value parameter, value receiver or range copy duplicates it bit for bit - including its held-or-not state and the queue bookkeeping. Two goroutines locking two copies exclude nobody: they serialise against different locks while writing to different copies of the fields, so the synchronisation quietly evaporates and, if the original was locked when copied, the copy starts life pretending to be held. `go vet` catches this with its `copylocks` analyzer, which reports things like *passes lock by value: Scoreboard contains sync.Mutex* for a value receiver such as `func (s Scoreboard) Total() int`. The fix is to use the type only through a pointer: pointer receivers on every method, `*Scoreboard` parameters, and `[]*Scoreboard` or `map[string]*Scoreboard` for collections. When callers genuinely want a snapshot, give them a method that takes the lock and returns a separate plain-data struct.
code
go · 12 linestype Scoreboard struct {
mu sync.Mutex
wins int
losses int
}
// go vet (copylocks): Total passes lock by value: Scoreboard contains sync.Mutex
func (s Scoreboard) Total() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.wins + s.losses
}go deeper
Know the rule: never copy a value that contains a sync.Mutex. Use pointer receivers and pass a pointer, and treat a go vet copylocks message as a real bug rather than noise.
Explain what the copy actually duplicates - the state word and waiter bookkeeping - and list the sites that copy silently: value receivers, value parameters, assignments, range variables and formatting calls.
Review for it: spot a diff that switches a receiver to a value, know that embedding hides a lock several layers down, and make go vet a required CI step rather than something an editor happens to show.
Decide the ownership shape for shared components across the codebase - lock-bearing types are pointer-only, snapshots are separate value types - so that the copy question never has to be re-litigated in individual reviews.
## What copying actually does `sync.Mutex` is a small struct of unexported fields - a state word and a semaphore token. Nothing in it is a pointer to shared state. That means Go's ordinary value semantics apply with no exceptions: every place a value is copied, the mutex is copied too. The places that copy, all of which look innocent: - a value receiver: `func (s Scoreboard) Total() int` - a value parameter: `func report(s Scoreboard)` - a plain assignment: `b := *a` or `snapshot = *board` - returning the struct by value - `for _, s := range boards` where `boards` is `[]Scoreboard` - storing into or reading out of `map[string]Scoreboard` or `[]Scoreboard` - capturing the value (not a pointer to it) in a closure - passing it to a variadic `any`, for example `fmt.Println(board)` After any of these you have two mutexes. Locking one does not exclude the other. If two goroutines each hold their own copy, both are inside the critical section at once, and both are also mutating *different* copies of the guarded fields - so the write that mattered is often thrown away as well. The bug does not announce itself; the code looks correctly locked on every line. Worse, the copy inherits the *state* of the original. Copy a struct while some goroutine holds its mutex and the copy is born locked, with a waiter count that describes a queue that does not exist. Nothing will ever unlock it. This is the origin of the package rule: **a Mutex must not be copied after first use.** ## What go vet reports The `copylocks` analyzer knows that anything containing a `sync.Locker` (a mutex, an RWMutex, a WaitGroup, a Cond, a `sync.Once`) must not be copied, and it walks assignments, calls, receivers, returns and range statements looking for one. A typical message: ```text ./scoreboard.go:21:7: Total passes lock by value: Scoreboard contains sync.Mutex ``` Two practical notes. First, the check is structural, not dynamic - vet flags the copy whether or not the mutex has ever been locked, which is right, because the whole point is that you cannot tell at the copy site. Second, do not assume it runs by itself: `go test` executes only a small, high-confidence subset of the vet suite. Put `go vet ./...` in CI so that a change which starts passing a lock-bearing struct by value is caught at review time rather than at 3am. ## The reviewer's rule When you see a diff that changes `func (s *Scoreboard) ...` into `func (s Scoreboard) ...`, or that starts passing a `Scoreboard` rather than a `*Scoreboard`, the question to ask is not *does this compile* (it does) or *is it faster* (it is not - copying a struct is more work than passing a word). The question is whether the type contains a lock, directly or through an embedded struct several layers down. That depth is the part people miss. A type that embeds another type that holds a mutex is itself uncopyable, and nothing in its own declaration says so. Vet knows; a reader skimming the struct usually does not. The rules that keep it simple: 1. **A lock-bearing type is a pointer type.** Every method takes `*T`, every parameter is `*T`, every collection holds `*T`. Mixing value and pointer receivers on such a type is a design smell. 2. **Do not return the whole struct.** If callers want the numbers, take the lock and return a separate, lock-free value type - `Stats{Wins, Losses int}` - built inside the critical section. 3. **Beware unaddressable homes.** `map[string]Scoreboard` cannot work: a map element is not addressable, so `m[k].mu.Lock()` will not even compile. Store `*Scoreboard` instead. 4. **Watch formatting and logging.** `fmt.Printf("%+v", board)` copies the struct into an `any`. Pass `&board`, or format the fields you care about. 5. **Give the zero value one home.** Because the zero value is usable, it is tempting to create scoreboards freely; make one and hand out its address, so the whole program shares a single lock and a single set of counters. ## Why the compiler does not simply forbid it Go has no notion of a non-copyable type, and adding one for this case would ripple through the whole type system. The chosen tradeoff is a plain value type plus a vet check, which is why `go vet` is not optional tooling here - for lock-bearing structs it is the only enforcement there is. Some codebases add a zero-width `noCopy` field for the same reason, which exists purely so vet has something to point at. The race detector will often surface the consequences too - two goroutines writing the same original object through paths that lost their exclusion - but it only reports races that actually occur in the run. Vet reports the copy itself, every time, which is why it is the cheaper of the two safety nets.
- Does go vet's copylocks check run automatically as part of go test?No. `go test` runs only a small, high-confidence subset of the vet analyzers, and `copylocks` is not in it. Editors surface it and `go vet ./...` reports it, so the reliable answer is a CI step that runs vet across the module. Relying on someone noticing it in their editor is how a value receiver on a lock-bearing type reaches production.
- A caller wants a snapshot of a struct that contains a sync.Mutex. What do you give them?Not a copy of the struct. Add a method with a pointer receiver that takes the lock, copies the guarded fields into a separate lock-free type, and returns that. The snapshot is then an ordinary value the caller can pass around, print and store freely, while the mutex and the live data stay behind a single pointer.
- Why can a struct containing a sync.Mutex not be used as a map value directly?Map elements are not addressable, so `m[k].mu.Lock()` does not compile - you cannot take the address a pointer-receiver call would need, and any read of `m[k]` produces a copy of the whole struct, mutex included. Store `map[string]*Scoreboard` so each entry is one shared object with one lock.
- How can a struct be uncopyable without visibly containing a sync.Mutex?By embedding, at any depth, something that contains a `sync.Locker` - a mutex, an RWMutex, a WaitGroup, a Cond or a Once. The outer type's own declaration shows nothing, which is exactly why the vet check matters: it walks the type graph, whereas a reviewer reads one struct literal and sees only plain fields.
saying these in an interview costs you the question
- Thinks sync.Mutex is a reference type like a map or channel
- Assumes two copies of the struct still share one lock
- Silences the vet warning instead of switching to a pointer receiver
- Mixes value and pointer receivers on a lock-bearing type
- Believes a copied mutex is always unlocked in the copy
- Stores lock-bearing structs by value in a slice or map