What happens when multiple goroutines access a map concurrently? How do you make it safe?
Question 59HardGo 1.22 to 1.25
Built-in maps are not safe for concurrent use if at least one goroutine writes. Concurrent reads alone are fine. The runtime has a cheap best-effort check (a hashWriting flag). When it catches a conflict it calls fatal("concurrent map writes") or reports "concurrent map read and map write". This is not a panic, so recover cannot catch it. The whole process crashes. Races the check misses can silently corrupt the map, so run tests with go test -race.
type SafeCounter struct {
mu sync.RWMutex
m map[string]int
}
func (c *SafeCounter) Inc(k string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k]++
}
func (c *SafeCounter) Get(k string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
Options, ranked:
sync.Mutex/RWMutexplus a map: the default. RWMutex only helps when reads dominate and critical sections are not trivially short.- Sharded maps: N maps, each with its own mutex, chosen by
hash(key) % N. This cuts contention under heavy write load. sync.Map: for specific access patterns (see the next question).- Confinement: a single owning goroutine that serves requests over channels.
Never copy a struct that contains a mutex; go vet catches this. Returning a map that the lock guards also leaks the race to callers. Return a maps.Clone instead.
More on Arrays, Slices, Maps & Strings
- Q57Why is map iteration order random, and how do you iterate deterministically?
- Q58Is it safe to add or delete map entries while ranging over the map?
- Q60When should you use sync.Map instead of a map with a mutex?
- 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?