Middle · Concurrency
A value receiver copies the mutex but shares the map
A cache struct holds sync.Mutex and map[string]int. Its Put method uses a value receiver and locks c.mu. Why can it still race?
Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.Mutex
values map[string]int
}
func (c *Cache) Put(k string, v int) {
c.mu.Lock()
defer c.mu.Unlock()
if c.values == nil {
c.values = map[string]int{}
}
c.values[k] = v
}
func (c *Cache) Snapshot() map[string]int {
c.mu.Lock()
defer c.mu.Unlock()
out := make(map[string]int, len(c.values))
for k, v := range c.values {
out[k] = v
}
return out
}
func main() {
var c Cache
c.Put("a", 1)
copy := c.Snapshot()
copy["a"] = 99
fmt.Println(c.Snapshot()["a"])
}Reveal a strong answer
The presence of a mutex in a struct does not prove that every operation locks the same mutex. A value receiver or assignment can copy the lock while retaining a reference to the same map. Likewise, sync.Map protects its own operations but cannot infer an invariant spanning several keys. Define the state that must change together and keep one synchronization owner for that state.
Use RWMutex when measured read contention justifies it; long-held read locks can still delay writers. A mutex protects the whole declared invariant only if every access participates in the same protocol. sync.Map is suited to documented use patterns such as mostly stable keys or disjoint key ownership, not an automatic replacement for a typed map with multi-step business operations. Both methods use pointer receivers so calls share the lock owner. Snapshot returns independent map storage rather than returning Cache by value or exposing the internal map. This example uses simple scalar values; references inside values would need their own ownership or deeper-copy contract.
Do not copy mutex-bearing values after use, embed them into casually copied API results or return internal maps after unlocking. go vet copylocks helps identify copied locks; native race tests exercise actual access paths. A race-free get-then-put can still duplicate a business action. Test lost updates, snapshots, simultaneous misses and compound invariants separately from the absence of a data-race report.
Common trap
The mere presence of a lock field does not prove that every access uses the same lock.
Try it
Refactor to one stable lock owner, return an independent snapshot and run native copylocks/race checks plus compound-invariant tests.
Worked solution
The receiver copy gets a different mutex but still points to the shared map storage, so callers do not synchronize on one owner. Use pointer receivers and avoid copying lock-bearing values after use. Protect all reads and writes under the declared protocol and copy exported mutable data deliberately. sync.Map is not a transaction across arbitrary keys, and a data-race-free check-then-act can still violate a business invariant.
Change the constraint
When would measured contention justify RWMutex rather than a simple Mutex?
How did you do?
Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.
Not marked. Progress stays on this browser, separate from course completion.
Primary references
Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.