Golang中map的安全使用

简单介绍

map数据类型在很多语言中都有,是一个key,value形式的hash表,从而将key,value进行一一映射,进行快速查找、添加、删除等操作。在Go语言中也不例外,提供了map数据结构类型。

内建map切忌开箱即用

golang中,map是引用类型,如指针切片一样,通过下面的代码声明后指向的是nil。这点在golang官方文档中也说明了,所以千万别直接声明后就使用,开始可能经常会犯下面的错:

1 var m map[string]string
2 m["result"] = "result"

上面的第一行代码并没有对map进行一个初始化,而却对其进行写入操作,就是对空指针的引用,这将会造成一个painc。所以,得记得用make函数对其进行分配内存和初始化:

1 m := make(map[string]string)
2 m["result"] = "result"

golang中的map并不是并发安全的
经常使用map,平时用着也很爽,但是突然某天流量上来了,程序不知不觉就挂了,还不清楚是怎么回事,明明以前用着好好的呀。所以有些好习惯在刚开始就养成,比如断言检查,并发安全考虑等。map纵然很好用,但也得谨慎。或许很多人还不知道,golang内建map其实并不是并发安全的,下面我自定义了一个结构体,赋其map属性,给其绑定Get,Set方法方便操作。请看下面代码:

 1 // M
 2 type M struct {
 3     Map    map[string]string
 4 }
 5  
 6 // Set ...
 7 func (m *M) Set(key, value string) {
 8     m.Map[key] = value
 9 }
10  
11 // Get ...
12 func (m *M) Get(key string) string {
13     return m.Map[key]
14 }
15  

上面的代码中,给一个结构体赋予了一个map属性,且绑定了两个方法,进行读写操作。当你在写了一个test在单个goroutine中跑的时候或许没什么问题,或许多个goroutine中执行也没问题。

 1 // TestMap
 2 func TestMap(t *testing.T) {
 3     c := helper.M{Map: make(map[string]string)}
 4     wg := sync.WaitGroup{}
 5     for i := 0; i < 10; i++ {
 6         wg.Add(1)
 7         go func(n int) {
 8             k, v := strconv.Itoa(n), strconv.Itoa(n)
 9             c.Set(k, v)
10             t.Logf("k=:%v,v:=%v\n", k, c.Get(k))
11             wg.Done()
12         }(i)
13     }
14     wg.Wait()
15     t.Log("ok finished.")
16 }

然而当你你再添加的时候,goroutine再增加的时候,会报下面的错,也就是map并发写入出错。

1 === RUN   TestMap
2 fatal error: concurrent map writes
3 
4 goroutine 15 [running]:
5 runtime.throw({0x1046b5c6f?, 0x0?})

如何解决

其实要解决上面的问题也不难,出错原因golang已经写得很清楚了,concurrent map writes,并发写map异常,这个时候肯定想的是并发操作上能不能解决。

很显然,我们可以用锁机制解决上面的问题。我们将上面的map结构改成如下:

 1 // M
 2 type M struct {
 3     Map  map[string]string
 4     lock sync.RWMutex // 加锁
 5 }
 6 
 7 // Set ...
 8 func (m *M) Set(key, value string) {
 9     m.lock.Lock()
10     defer m.lock.Unlock()
11     m.Map[key] = value
12 }
13 
14 // Get ...
15 func (m *M) Get(key string) string {
16     defer m.lock.Unlock()
17     m.lock.Lock()
18     return m.Map[key]
19 }

在上面的代码中,我们引入了锁机制操作,从而保证了map在多个goroutine中的安全。这时再执行我们的test会发现其正常输出。

1     bbb_test.go:38: k=:37,v:=37
2     bbb_test.go:38: k=:40,v:=40
3     bbb_test.go:38: k=:93,v:=93
4     bbb_test.go:38: k=:39,v:=39
5     bbb_test.go:38: k=:41,v:=41
6     bbb_test.go:38: k=:90,v:=90
7     bbb_test.go:43: ok finished.
8 --- PASS: TestMap1 (0.00s)
9 PASS

或许你可以尝试下sync.Map

golang中的sync.Map是并发安全的,其实也就是sync包中golang自定义的一个名叫Map的结构体。结构体原型如下:

1 type Map struct {
2    mu Mutex
3    read atomic.Value // readOnly
4    dirty map[interface{}]*entry
5    misses int
6 }

可以看见有 Mutex,很显然也是用了锁机制的,从而来保证了并发安全。该包中的Map提供了Store、Load、Delete、Range等操。并且sync包中的Map是开箱可用的,也即是声明后就可以直接使用,如下:

 1 // TestMap2  ...
 2 func TestMap2(t *testing.T) {
 3     var m sync.Map
 4     wg := sync.WaitGroup{}
 5     for i := 0; i < 100; i++ {
 6         wg.Add(1)
 7         go func(n int) {
 8             k, v := strconv.Itoa(n), strconv.Itoa(n)
 9             m.Store(k, v)
10             value, _ := m.Load(k)
11             t.Logf("key=%v,value=%v\n", k, value)
12             wg.Done()
13         }(i)
14     }
15     wg.Wait()
16     t.Log("ok finished.")
17 }

结果如下:

1     abc_test.go:43: key=80,value=80
2     abc_test.go:43: key=47,value=47
3     abc_test.go:43: key=63,value=63
4     abc_test.go:43: key=97,value=97
5     abc_test.go:48: ok finished.
6 --- PASS: TestMap2 (0.00s)
7 PASS
posted @ 2023-05-07 23:40  时间的漩涡1992  阅读(22)  评论(0编辑  收藏  举报