Go 映射:sync.Map 与并发安全
Go 语言中的内置 map 类型在多个 goroutine 同时读写时会发生数据竞争(data race),导致程序崩溃或结果不可预测。为了解决这个问题,Go 标准库提供了 sync.Map —— 一个专为并发场景设计的线程安全映射类型。本文将手把手教你如何正确使用 sync.Map,以及何时该用它、何时不该用。
1. 理解普通 map 的并发问题
不要在多个 goroutine 中同时对普通 map 执行写操作,即使有读操作也不行。
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[string]int)
go func() {
for i := 0; i < 1000; i++ {
m["count"] = i // 写操作
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m["count"] // 读操作
}
}()
time.Sleep(time.Second)
fmt.Println(m["count"])
}
运行上述代码时,极大概率会触发 panic,提示 “concurrent write to map”。这是因为 Go 的运行时检测到了并发写入。
2. 使用 sync.Map 实现并发安全
sync.Map 是 Go 标准库 sync 包提供的并发安全映射类型。它内部通过分段锁和原子操作优化性能,适合读多写少的场景。
基本操作步骤:
-
声明一个
sync.Map变量:var sm sync.Map -
写入键值对:调用
Store方法。sm.Store("key1", 42) -
读取值:调用
Load方法,它返回(value, ok)。if val, ok := sm.Load("key1"); ok { fmt.Println(val) // 输出 42 } -
删除键:调用
Delete方法。sm.Delete("key1") -
仅当不存在时写入:调用
LoadOrStore。actual, loaded := sm.LoadOrStore("key1", 100) // 如果 key1 不存在,则存入 100,并返回 (100, false) // 如果已存在,则返回 (现有值, true)
完整示例:
package main
import (
"fmt"
"sync"
)
func main() {
var sm sync.Map
// 写入
sm.Store("name", "Alice")
sm.Store("age", 30)
// 读取
if name, ok := sm.Load("name"); ok {
fmt.Println("Name:", name)
}
// 并发安全地累加计数器
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 尝试加载当前值,若无则设为0,然后+1再存回
for {
old, loaded := sm.Load("counter")
var newVal int
if !loaded {
newVal = 1
} else {
newVal = old.(int) + 1
}
// 使用 CompareAndSwap 尝试更新(但 sync.Map 没有 CAS)
// 所以这里改用 LoadOrStore 不合适,应直接 Store(可能覆盖)
// 更好的方式:使用 atomic 或 mutex 保护整数
// 此处仅为演示 Store 安全性
sm.Store("counter", newVal)
break // 简化逻辑,实际高并发需重试机制
}
}()
}
wg.Wait()
if counter, ok := sm.Load("counter"); ok {
fmt.Println("Final counter:", counter)
}
}
注意:
sync.Map的值是interface{}类型,使用时需要做类型断言(如old.(int))。这会带来运行时开销和潜在 panic 风险。
3. sync.Map 的适用场景与限制
sync.Map 并非万能。官方文档明确指出:仅在以下情况推荐使用:
- 读操作远多于写操作;
- 多个 goroutine 同时访问不同的键;
- 键的生命周期较长(不会频繁增删)。
反之,如果满足以下任一条件,应优先考虑使用普通 map + sync.RWMutex:
- 需要强类型(避免
interface{}); - 需要遍历所有键值对(
sync.Map的Range效率较低); - 写操作频繁或键动态变化剧烈;
- 对性能要求极高且可接受手动加锁。
性能对比参考(定性)
| 场景 | 推荐方案 |
|---|---|
| 高频读、低频写、键固定 | sync.Map |
| 需要遍历、强类型、高频写 | map + sync.RWMutex |
4. 正确使用 sync.Map 的关键细节
-
不要复制 sync.Map:
sync.Map包含内部状态(如 mutex 或指针),复制会导致未定义行为。始终以指针或值的方式传递(Go 中 struct 默认按值传递,但sync.Map设计为可安全按值使用,因其内部使用指针共享状态)。不过,最佳实践是将其作为字段嵌入结构体,或通过指针共享。 -
Range 遍历时不能修改:
调用sm.Range(func(key, value interface{}) bool)时,在回调函数中不要调用Store或Delete,否则可能导致死锁或跳过元素。 -
零值即有效:
var sm sync.Map直接可用,无需初始化。 -
不保证顺序:
和普通 map 一样,遍历顺序是随机的。
5. 替代方案:使用 RWMutex 保护普通 map
当 sync.Map 不适用时,手动加锁更灵活高效:
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
func (sm *SafeMap) Load(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
val, ok := sm.m[key]
return val, ok
}
func (sm *SafeMap) Store(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.m, key)
}
这种方式支持强类型、高效遍历,且逻辑清晰。
6. 如何选择?
判断流程如下:
-
是否需要并发读写同一个 map?
→ 否:直接用普通map。
→ 是:进入下一步。 -
是否满足以下全部条件?
- 读 >> 写
- 键基本不变(很少新增/删除)
- 不需要遍历或遍历频率极低
- 能接受
interface{}类型
→ 是:使用sync.Map。
→ 否:使用map + sync.RWMutex。
记住:过早优化是万恶之源。除非你已通过性能分析确认 map 是瓶颈,否则优先选择简单、类型安全的 RWMutex 方案。

暂无评论,快来抢沙发吧!