When should you use sync.Map instead of a map with a mutex?
Question 60HardGo 1.22 to 1.25
The sync.Map docs name two cases where it wins:
- The entry for a key is written once and read many times, as in caches that only grow.
- Many goroutines read, write, and overwrite disjoint sets of keys.
In those cases it avoids lock contention on a single mutex, which matters on machines with many cores. Everywhere else a plain map with a mutex is usually faster, is type-safe, and supports len.
Go 1.24 reimplemented sync.Map on a concurrent hash-trie (internal/sync.HashTrieMap). That improved performance for mixed workloads and for disjoint writes. The old version used a read-only map plus a dirty map with promotion.
var cache sync.Map // zero value ready; do not copy after use
func getUser(id int) *User {
if v, ok := cache.Load(id); ok {
return v.(*User) // type assertion: no generics
}
u := loadUser(id)
actual, _ := cache.LoadOrStore(id, u) // atomic; losers get the winner's value
return actual.(*User)
}
// Other APIs: Store, Delete, LoadAndDelete, Swap, CompareAndSwap, CompareAndDelete,
// Range(func(k, v any) bool), Clear (Go 1.23)
Downsides:
- It uses
any, so every access needs a type assertion. Wrap it in a small generic type to get type safety. - There is no
Len(). Rangeis not a consistent snapshot.- Values boxed in interfaces can cause extra allocations.
LoadOrStorestill runs the costlyloadUseron every racing goroutine. Usegolang.org/x/sync/singleflightto deduplicate the work.
More on Arrays, Slices, Maps & Strings
- Q58Is it safe to add or delete map entries while ranging over the map?
- Q59What happens when multiple goroutines access a map concurrently? How do you make it safe?
- Q61Why does m["k"].Field = 1 fail to compile for map[string]T where T is a struct?
- Q62Why can't you take the address of a map element (&m[k]), when you can with slice elements?
- Q63Which types can be map keys? What happens with interface keys and NaN?
- Q64Do Go maps shrink after deleting entries? How do you reclaim memory?