17.6 sync.Map: Concurrent Map Without External Locking
Right, so you need a map. And you need to smash it from a dozen goroutines at once. Your first instinct is to reach for a map and wrap every access in a Mutex, which is a perfectly respectable, grown-up choice. But then you see sync.Map in the standard library and think, “Ooh, a shiny, lock-free, concurrent map! My problems are solved!”
Hold that thought. sync.Map is a fantastic tool, but it’s not a drop-in replacement for your standard map[string]interface{} with a mutex. It’s a specialized tool for a specific set of jobs, and if you use it for the wrong one, you’ll end up with something far more complicated and slower than what you started with. Let’s break down exactly when you should—and more importantly, shouldn’t—use this thing.
What Problem Does It Actually Solve?
The sync.Map is optimized for two key scenarios that make a regular map with a mutex perform poorly. First, when you have a given key that’s only ever written once but read many times (think caches populated at startup). Second, when different goroutines are accessing disjoint sets of keys, meaning each goroutine is playing with its own unique keys.
Why is a mutex bad here? Because every single time a reader goroutine wants to read a key, it has to acquire a lock. If you have a hugely read-heavy workload, that lock becomes a major point of contention. sync.Map avoids this by using a clever trick: it uses atomic operations and read-only tables for reads whenever possible, so most of the time, your readers don’t block each other at all. It only brings out the big, blocking mutex when it absolutely has to, during a write.
The Bizarre and Wonderful API
Forget everything you know about map. The API is… different. It’s not a pleasure cruise; it’s a tool designed for a job. You don’t get fancy range loops or len(). Instead, you get these three core methods.
var m sync.Map
// Store a value (the equivalent of m["key"] = value)
m.Store("answer", 42)
// Load a value (the equivalent of value, ok := m["key"])
value, ok := m.Load("answer")
if ok {
fmt.Println("The answer is:", value.(int)) // Note: you get an interface{} back!
}
// Delete a value (the equivalent of delete(m, "key"))
m.Delete("answer")
See the catch? You get an interface{} back from Load, so you’re back to type assertions. This is the first clue that this isn’t your everyday map.
The Weirder Stuff: LoadOrStore and Range
This is where sync.Map earns its keep. These methods are atomic powerhouses.
LoadOrStore is the “get me the value, but if it’s not there, put this one in and then give it to me” operation. Doing this correctly with a regular mutexed map is error-prone. With sync.Map, it’s one atomic call.
// This is atomic. Only one goroutine will get (nil, false) and actually store the value.
actual, loaded := m.LoadOrStore("question", "What is the meaning of life?")
if !loaded {
fmt.Println("Stored the question for the first time:", actual)
}
Then there’s Range. This is how you iterate, and it’s a testament to the weirdness inside. It takes a function func(key, value interface{}) bool and calls it for every key-value pair. If your function returns false, it stops iteration. The documentation explicitly notes that it may or may not reflect any concurrent Store or Delete operations. It’s a snapshot, but a weird, non-consistent one.
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true // keep going
})
When To Use It (And When To Run Away)
Use sync.Map when:
- Your map is a read-heavy cache, populated once and read millions of times.
- Different goroutines are storing and accessing completely different keys. Think a connection-per-goroutine pattern where each goroutine stores its own state under a unique connection ID.
Do NOT use sync.Map when:
- You need to
len()or range over a consistent snapshot. You can’t. - Your access patterns are write-heavy or all goroutines are constantly updating the same keys. You’ll lose the optimization and just incur the overhead of the complex internals.
- You want a simple, obvious code path. A
struct { sync.Mutex; m map[string]string }is often easier to reason about.
The brutal truth is that for probably 90% of cases, a plain old mutex and a standard map are simpler, faster, and more understandable. sync.Map is a precision instrument. Pull it out when you’ve measured a problem and you know it fits one of the two scenarios it was designed for. Otherwise, you’re just showing off a fancy tool you don’t know how to use. And I know you’re better than that.