在Golang的并发编程场景中,很多开发者都会遇到需要多个goroutine同时操作Map的需求,但原生map本身并不具备并发安全特性,直接并发读写会导致程序崩溃。下面我们就来介绍几种实现并发安全Map操作的常用方案。

方案一:使用互斥锁封装原生Map
互斥锁是最基础的并发控制手段,我们可以通过结构体封装原生map和sync.Mutex,所有对map的操作都先获取锁再执行,保证同一时间只有一个goroutine能操作map。
这种方案适合并发量不高、读写操作都比较频繁的场景,实现逻辑简单,容易理解。
package main
import (
"fmt"
"sync"
)
// SafeMap 用互斥锁封装的并发安全Map
type SafeMap struct {
mu sync.Mutex
m map[string]int
}
// NewSafeMap 初始化SafeMap
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
// Set 设置键值对
func (s *SafeMap) Set(key string, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}
// Get 获取键值对,返回值和是否存在的标识
func (s *SafeMap) Get(key string) (int, bool) {
s.mu.Lock()
defer s.mu.Unlock()
val, ok := s.m[key]
return val, ok
}
// Delete 删除键
func (s *SafeMap) Delete(key string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.m, key)
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
// 启动10个goroutine并发写
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key_%d", idx), idx)
}(i)
}
wg.Wait()
// 读取验证
val, ok := sm.Get("key_5")
if ok {
fmt.Printf("key_5的值为:%d\n", val)
}
}方案二:使用读写锁优化读多写少场景
如果业务中读操作的频率远高于写操作,使用互斥锁会让所有读操作也串行执行,浪费性能。这时候可以用sync.RWMutex读写锁,读操作加读锁,写操作加写锁,多个读操作可以并行执行,只有写操作会阻塞其他读写。
下面是基于读写锁实现的并发安全Map:
package main
import (
"fmt"
"sync"
)
// RWSafeMap 用读写锁封装的并发安全Map
type RWSafeMap struct {
rw sync.RWMutex
m map[string]int
}
// NewRWSafeMap 初始化RWSafeMap
func NewRWSafeMap() *RWSafeMap {
return &RWSafeMap{
m: make(map[string]int),
}
}
// Set 写操作,加写锁
func (r *RWSafeMap) Set(key string, value int) {
r.rw.Lock()
defer r.rw.Unlock()
r.m[key] = value
}
// Get 读操作,加读锁
func (r *RWSafeMap) Get(key string) (int, bool) {
r.rw.RLock()
defer r.rw.RUnlock()
val, ok := r.m[key]
return val, ok
}
// Delete 写操作,加写锁
func (r *RWSafeMap) Delete(key string) {
r.rw.Lock()
defer r.rw.Unlock()
delete(r.m, key)
}
func main() {
rsm := NewRWSafeMap()
// 先写入数据
rsm.Set("test", 100)
var wg sync.WaitGroup
// 启动20个goroutine并发读
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
val, ok := rsm.Get("test")
if ok {
fmt.Println(val)
}
}()
}
wg.Wait()
}方案三:使用官方sync.Map
Golang 1.9之后官方在sync包中提供了sync.Map类型,这是官方专门实现的并发安全Map,底层做了很多性能优化,适合两种场景:一是读多写少且键值对生命周期较长的场景,二是多个goroutine读写不同键的场景。
sync.Map提供了Store、Load、Delete、LoadOrStore等常用方法,使用起来不需要自己加锁:
package main
import (
"fmt"
"sync"
)
func main() {
var sm sync.Map
var wg sync.WaitGroup
// 并发写
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
sm.Store(fmt.Sprintf("key_%d", idx), idx)
}(i)
}
// 并发读
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
val, ok := sm.Load(fmt.Sprintf("key_%d", idx))
if ok {
fmt.Printf("key_%d的值为:%v\n", idx, val)
}
}(i)
}
wg.Wait()
// 尝试加载或存储,如果键不存在就存储
val, loaded := sm.LoadOrStore("key_10", 10)
if loaded {
fmt.Printf("key_10已存在,值为:%v\n", val)
} else {
fmt.Println("key_10存储成功")
}
}不同方案的选择建议
我们可以根据实际场景选择合适的方案,下面是简单的对比参考:
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 互斥锁封装原生Map | 并发量不高,读写频率均衡 | 实现简单,逻辑清晰,兼容原生map的所有操作 | 读写都串行,高并发下性能一般 |
| 读写锁封装原生Map | 读多写少的场景 | 读操作可以并行,读性能优于互斥锁方案 | 写操作还是会阻塞所有读写,写频繁时性能下降 |
| sync.Map | 读多写少、键值对生命周期长,或多goroutine操作不同键 | 官方实现,性能优化好,不需要手动加锁 | 不支持len方法,不能直接遍历,部分场景不如原生map灵活 |
需要注意的是,sync.Map虽然方便,但并不是所有场景都适合使用,如果业务中对map的操作比较简单,且并发量不高,用锁封装原生map反而更直观,也更容易维护。