Concurrency · 3 MIN READ
Mutex, RWMutex and sync.Map: protect the invariant and do not copy the lock
Choose synchronization around related state and return snapshots rather than copied lock owners.
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.
concurrency · A MAP OF THE IDEA
- Define invariant
- Keep one lock owner
- Copy returned data
- Test races and business outcomes
Useful when
- Choose synchronization around related state and return snapshots rather than copied lock owners.
- Makes the owner and the failure boundary explicit.
Watch out
- Do not copy mutex-bearing values after use, embed them into casually copied API results or return internal maps after unlocking.
- Run native service and failure tests in your own development environment.
Implementation & operation cost: Additional state and tests must be justified by the demonstrated failure; this lesson is not a requirement for every Go program.
The contract and the failure
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.
Work through the example
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.
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"])
}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.
Production decisions
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.
Explain it in an interview
A struct with a mutex and a map uses a value receiver. What can go wrong? The lock is copied while the map storage remains shared. Use a stable lock owner and pointer receivers; test both races and the compound invariant. Describe the state before the operation, its owner, the failure between steps and the evidence you would test. Distinguish what this example demonstrates from the infrastructure a production service still needs.
TAKE THIS WITH YOU
Choose synchronization around related state and return snapshots rather than copied lock owners. The lock is copied while the map storage remains shared. Use a stable lock owner and pointer receivers; test both races and the compound invariant.
Check your understanding
A struct with a mutex and a map uses a value receiver. What can go wrong?
Go to the source
Progress stays in this browser. No account needed.