在Golang的并发编程场景中,哈希Map是常用的数据结构,但原生map不支持并发读写,多个goroutine同时对其进行读和写操作会直接触发panic。因此我们需要找到合适的方案,在保证并发安全的前提下高效读取哈希Map中的数据。

原生Map的并发限制
Golang的原生map设计上就不是并发安全的,官方文档明确说明多个goroutine同时读写map会导致未定义行为。我们可以通过一段简单的代码验证这个问题:
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[int]int)
// 启动写协程
go func() {
for i := 0; i < 1000; i++ {
m[i] = i
time.Sleep(time.Microsecond)
}
}()
// 启动读协程
go func() {
for i := 0; i < 1000; i++ {
_ = m[i]
time.Sleep(time.Microsecond)
}
}()
time.Sleep(time.Second * 2)
fmt.Println("程序执行完成")
}
运行上述代码大概率会触发fatal error: concurrent map read and map write的错误,因此并发场景下不能直接使用原生map进行读写操作。
基于sync.RWMutex实现并发安全读取
如果我们的场景是读多写少,使用读写锁sync.RWMutex是最常用的方案。读写锁的特点是多个读操作可以同时进行,只有写操作需要独占锁,能大幅提升读多写少场景下的性能。
实现示例
我们可以封装一个带读写锁的Map结构体,对外提供安全的读写方法:
package main
import (
"fmt"
"sync"
"time"
)
// SafeMap 并发安全的哈希Map封装
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
// NewSafeMap 初始化安全Map
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
// Set 写操作,加写锁
func (s *SafeMap) Set(key string, val int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = val
}
// Get 读操作,加读锁
func (s *SafeMap) Get(key string) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.m[key]
return val, ok
}
func main() {
safeMap := NewSafeMap()
// 启动写协程
go func() {
for i := 0; i < 10; i++ {
safeMap.Set(fmt.Sprintf("key_%d", i), i)
time.Sleep(time.Millisecond * 100)
}
}()
// 启动多个读协程
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 10; j++ {
val, ok := safeMap.Get(fmt.Sprintf("key_%d", j))
if ok {
fmt.Printf("协程%d读取到key_%d的值为%dn", id, j, val)
}
time.Sleep(time.Millisecond * 50)
}
}(i)
}
time.Sleep(time.Second * 2)
}
方案特点
- 读操作使用读锁,多个读协程可以并行执行,性能优于全局互斥锁
- 适用读多写少的场景,写操作仍会阻塞所有读和写操作
- 兼容原生map的所有操作,可灵活扩展其他map方法
使用sync.Map实现并发安全读取
Golang 1.9之后官方提供了sync.Map类型,是专门为并发场景设计的哈希Map,内部做了很多性能优化,适合两种典型场景:一是读多写少且键值对生命周期较短的场景,二是多个goroutine读写不同键的场景。
实现示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
// 写操作
go func() {
for i := 0; i < 10; i++ {
m.Store(fmt.Sprintf("key_%d", i), i)
time.Sleep(time.Millisecond * 100)
}
}()
// 读操作
for i := 0; i < 5; i++ {
go func(id int) {
for j := 0; j < 10; j++ {
val, ok := m.Load(fmt.Sprintf("key_%d", j))
if ok {
fmt.Printf("协程%d读取到key_%d的值为%dn", id, j, val)
}
time.Sleep(time.Millisecond * 50)
}
}(i)
}
time.Sleep(time.Second * 2)
}
方案特点
- 内部采用分离锁和冗余存储的设计,读操作性能非常好
- 不需要初始化,直接声明即可使用,API设计简洁
- 不适合存储大量数据,且无法直接获取map长度、遍历所有键等操作不如原生map方便
两种方案的选择建议
我们可以通过下表对比两种方案的适用场景:
| 对比维度 | 基于sync.RWMutex的封装Map | sync.Map |
|---|---|---|
| 适用场景 | 读多写少,需要完整map操作能力 | 读多写少且键值对生命周期短,或大量不同键读写 |
| 读性能 | 较好,读操作并行 | 优秀,内部优化更充分 |
| 写性能 | 写操作会阻塞所有读写 | 写操作影响相对较小 |
| 功能完整性 | 可灵活扩展所有原生map功能 | 仅提供基础Store、Load、Delete等方法 |
如果业务中需要频繁遍历map、获取map长度,或者需要自定义更多map相关操作,优先选择基于sync.RWMutex的封装方案。如果是临时缓存、键值对更新不频繁且不需要复杂操作的场景,sync.Map是更简单的选择。
注意事项
- 无论使用哪种方案,都不要在读取到值之后直接对值进行修改,如果值是引用类型,需要先加锁拷贝再操作,避免并发修改引发问题
- sync.Map的LoadOrStore方法可以实现不存在则写入的逻辑,避免先读后写的竞态问题
- 不要在锁内部执行耗时操作,尽量缩短加锁时间,提升整体并发性能
Golang并发安全哈希_Mapsync.RWMutex修改时间:2026-06-16 06:12:20