文章目录

Go 映射:sync.Map 与并发安全

发布于 2026-04-03 18:50:18 · 浏览 3 次 · 评论 0 条

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 包提供的并发安全映射类型。它内部通过分段锁和原子操作优化性能,适合读多写少的场景。

基本操作步骤:

  1. 声明一个 sync.Map 变量:

    var sm sync.Map
  2. 写入键值对:调用 Store 方法。

    sm.Store("key1", 42)
  3. 读取值:调用 Load 方法,它返回 (value, ok)

    if val, ok := sm.Load("key1"); ok {
        fmt.Println(val) // 输出 42
    }
  4. 删除键:调用 Delete 方法。

    sm.Delete("key1")
  5. 仅当不存在时写入调用 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.MapRange 效率较低);
  • 写操作频繁或键动态变化剧烈;
  • 对性能要求极高且可接受手动加锁。

性能对比参考(定性)

场景 推荐方案
高频读、低频写、键固定 sync.Map
需要遍历、强类型、高频写 map + sync.RWMutex

4. 正确使用 sync.Map 的关键细节

  1. 不要复制 sync.Map
    sync.Map 包含内部状态(如 mutex 或指针),复制会导致未定义行为。始终以指针或值的方式传递(Go 中 struct 默认按值传递,但 sync.Map 设计为可安全按值使用,因其内部使用指针共享状态)。不过,最佳实践是将其作为字段嵌入结构体,或通过指针共享

  2. Range 遍历时不能修改
    调用 sm.Range(func(key, value interface{}) bool) 时,在回调函数中不要调用 StoreDelete,否则可能导致死锁或跳过元素。

  3. 零值即有效
    var sm sync.Map 直接可用,无需初始化。

  4. 不保证顺序
    和普通 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. 如何选择?

判断流程如下

  1. 是否需要并发读写同一个 map
    → 否:直接用普通 map
    → 是:进入下一步。

  2. 是否满足以下全部条件

    • 读 >> 写
    • 键基本不变(很少新增/删除)
    • 不需要遍历或遍历频率极低
    • 能接受 interface{} 类型
      → 是:使用 sync.Map
      → 否:使用 map + sync.RWMutex

记住:过早优化是万恶之源。除非你已通过性能分析确认 map 是瓶颈,否则优先选择简单、类型安全的 RWMutex 方案。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文