文章目录

Go语言Map并发读写panic的复现与sync.Map的替代方案

发布于 2026-05-15 15:21:41 · 浏览 6 次 · 评论 0 条

Go语言Map并发读写panic的复现与sync.Map的替代方案

Go语言的原生map并不支持并发读写操作。如果在多个协程中同时对同一个map进行读写,程序会直接触发panic导致崩溃。本文将指导你如何复现这一问题,并提供两种标准解决方案。


一、 复现并发读写Panic

通过一段简单的代码,可以快速触发map的并发读写冲突。

  1. 创建 一个名为 main.go 的文件。

  2. 复制 以下代码到文件中。这段代码启动了两个协程,一个负责写入,一个负责读取,且没有任何保护机制。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        // 声明一个map
        m := make(map[int]string)
    
        // 协程1:并发写入
        go func() {
            for i := 0; i < 1000; i++ {
                m[i] = "write"
            }
        }()
    
        // 协程2:并发读取
        go func() {
            for i := 0; i < 1000; i++ {
                _ = m[i]
            }
        }()
    
        // 等待一段时间让协程执行
        time.Sleep(time.Second)
        fmt.Println(" finished")
    }
  3. 打开 终端或命令行工具。

  4. 执行 go run main.go 命令。

  5. 观察 输出结果。屏幕上将显示类似以下的错误信息,随即程序退出:

    fatal error: concurrent map read and map write
    ...

这证明了原生map在并发环境下是不安全的。


二、 方案一:使用互斥锁 (sync.RWMutex)

这是最通用的解决方案,适用于绝大多数需要并发访问map的场景。通过加锁,确保同一时间只有一个协程能操作map。

  1. 定义 一个包含 sync.RWMutex 和 map 的结构体。

  2. 封装 读写方法。写入 时使用 Lock()Unlock()读取 时使用 RLock()RUnlock()

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    // SafeMap 封装了带锁的map
    type SafeMap struct {
        data map[int]string
        mu   sync.RWMutex
    }
    
    // Set 写入数据
    func (sm *SafeMap) Set(key int, value string) {
        sm.mu.Lock()         // 加写锁
        sm.data[key] = value
        sm.mu.Unlock()       // 解写锁
    }
    
    // Get 读取数据
    func (sm *SafeMap) Get(key int) (string, bool) {
        sm.mu.RLock()        // 加读锁
        defer sm.mu.RUnlock()
        val, ok := sm.data[key]
        return val, ok
    }
    
    func main() {
        sm := SafeMap{
            data: make(map[int]string),
        }
    
        // 并发写入
        go func() {
            for i := 0; i < 1000; i++ {
                sm.Set(i, "value")
            }
        }()
    
        // 并发读取
        go func() {
            for i := 0; i < 1000; i++ {
                sm.Get(i)
            }
        }()
    
        time.Sleep(time.Second)
        fmt.Println("Program finished successfully")
    }
  3. 运行 代码。程序将正常结束,不再报错。


三、 方案二:使用 sync.Map

从Go 1.9版本开始,标准库提供了 sync.Map。它是专门为并发场景设计的,无需手动加锁,适合读多写少、或Key集合相对稳定的场景。

  1. 声明 一个 sync.Map 变量(不需要使用 make 初始化)。

  2. 调用 内置方法操作数据:

    • 写入:使用 Store(key, value)
    • 读取:使用 Load(key)
    • 遍历:使用 Range(f func(key, value interface{}) bool))
    • 删除:使用 Delete(key)
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    func main() {
        // 声明 sync.Map,无需make
        var m sync.Map
    
        // 并发写入
        go func() {
            for i := 0; i < 1000; i++ {
                m.Store(i, "value") // 使用 Store 方法
            }
        }()
    
        // 并发读取
        go func() {
            for i := 0; i < 1000; i++ {
                // Load 返回 interface{} 和 bool
                val, ok := m.Load(i)
                if ok {
                    _ = val.(string)
                }
            }
        }()
    
        time.Sleep(time.Second)
    
        // 遍历查看结果
        count := 0
        m.Range(func(key, value interface{}) bool {
            count++
            return true
        })
    
        fmt.Printf("Program finished. Total keys: %d\n", count)
    }
  3. 运行 代码。程序将安全执行并发读写操作。


四、 方案对比与选择

为了帮助你选择合适的方案,以下是两种实现方式的特性对比。

特性 map + sync.RWMutex sync.Map
类型安全 是 (编译期检查) 否 (使用 interface{},需类型断言)
内存开销 较低 较高 (内部冗余结构)
适用场景 通用场景,读写比例均衡 读多写少,Key集合稳定
使用复杂度 中等 (需手动加锁) 低 (调用内置方法)
无锁读取 否 (需加读锁) 是 (原子操作)

决策建议

  1. 如果你的业务逻辑涉及大量的更新操作,或者需要严格的类型检查,请优先选择 方案一 (map + sync.RWMutex)
  2. 如果你的场景是典型的缓存系统(大量读取,少量更新,Key相对固定),为了降低锁竞争带来的性能损耗,请选择 方案二 (sync.Map)

评论 (0)

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

扫一扫,手机查看

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