golang map tips

据说map应该是人类发明的一种最有效的数据结构了,再回头整理这篇,在golang中,使用map的时候多多少少总会遇到一些小问题,比如可达性问题,地址分配问题,这里一起记录一下。

地址可达性

在go playground中有这样的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
type Type struct {
A string
}
func main() {
items := make(map[string]Type)
items["q"] = Type{}
items["q"].A = "abc"
fmt.Println(items)
}

这样在执行的时候会报一个常见的错误:cannot assign to items[“q”].A

改变一下value的声明方式,之后再进行类似的操作就可以修改了:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
type Type struct {
A string
}
func main() {
items := make(map[string]*Type)
items["q"] = &Type{}
items["q"].A = "abc"
fmt.Printf("%+v", items)
}

在golang设计的时候,map中的value值应该是地址不可达的,就是说直接取map中的元素的地址会报错,比如把上面例子中的main函数改成下边这样:

1
2
3
4
itemsb := make(map[string]Type)
itemsb["p"] = Type{3}
pointer := &itemsb["p"]
fmt.Println(pointer)

会报出cannot take the address of itemsb[“p”]的错误。原因大致是因为,在golang中,一个容量不断增长的map可能会导致原来map中的一些元素发生rehashing,使得他们被重新分配到新的storage location上,这样可能会导致原先得到的address变得不可用。就是所谓的map member 的 not addresable。

正如这个答案中写的,map的indexing操作本来就是地址不可达的,这和golang中map的具体实现机制有关,golang中的map并没有保证它们的value值的地址是不可变的,因为value值的地址很有可能被重新分配,就像前面所说的那样。一个修改的办法就是把value值设置成为指针的形式,这样就相当于添加了一个额外的entry,即使真正需要的那个值的位置发生了变化,也可以redirection过去。以后使用map声明一个结构体的value值的时候,这一点要特别注意下。

对于slice的index操作就是地址可达的,对于map则是不可达的,总是使用map的时候要特别注意下。

map is a reference to the map contents, but it does not hold references (unless it explicitly stores pointers)

动态修改map中的元素

就像上面部分所介绍的,如果map中的value是一个struct元素,那么是没有办法直接修改其值的,因为是地址不可达的。如果想要修改,就需要生成一个新的结构体实例,然后把旧的实例替换掉。

删除map中的元素

就是直接使用delete关键字进行操作就ok了

http://stackoverflow.com/questions/23368843/how-to-delete-a-key-from-a-map-in-go-golang-using-reflection?rq=1

concurrent map writes

golang中的map并不是线程安全的(c/c++的stl也不是,大多是出于效率的考量),感觉自己在第一次写一个程序的时候,很少会考虑到线程安全这种事情,google一下,甚至是好多评价比较高的开源项目也会有类似的错误,因为这个错误带有一定的随机性。

其实具体解决的方案,也比较简单,就是找到具体的对数据进行读写操作的地方,然后加上个锁,比如参考beego里面一个类似问题的发现解决

首先需要定位一下,在哪种情况下,会发生这个错误,参考这里,我们根据这个例子来重现下cuncurrent map write的错误,下面这个程序和以认为是并发情况的一个测试,i从1到100遍历,每次可以认为i是同时并发(模拟的并发)的对map进行操作的goroutine,每次都创建出i个goroutine,对map进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import (
"sync"
)
var (
//mapLock sync.RWMutex
)
func main() {
const workers = 100
var wg sync.WaitGroup
wg.Add(workers)
m := map[int]int{}
for i := 1; i <= workers; i++ {
go func(i int) {
for j := 0; j < i; j++ {
//mapLock.Lock()
m[i]++
log.Println("job finished")
//mapLock.Unlock()
}
wg.Done()
}(i)
}
wg.Wait()
}
/*output
...
1 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
concurrent map read and map write2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
2016/07/21 12:19:36 job finished, current worker num: 35
...
*/

由于goroutine的调度机制的问题,出现concurrent map read and map write的时机是不确定的,但是在没有加锁的时候,会以很大的概览出现这个问题。

之后把注释的内容恢复过来,在map读写的时候加上锁,然后再进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
"log"
"sync"
)
var (
mapLock sync.RWMutex
)
func main() {
const workers = 100
var wg sync.WaitGroup
wg.Add(workers)
m := map[int]int{}
for i := 1; i <= workers; i++ {
go func(i int) {
for j := 0; j < i; j++ {
mapLock.Lock()
m[i]++
mapLock.Unlock()
log.Println("job finished, current worker num: ", i)
}
wg.Done()
}(i)
}
wg.Wait()
}

按照上述的方式来使用代码,多次测试也没有出现异常,都是被正常调度运行。

注意 留意下这里waitgroup的使用,对于goroutine的测试,使用waitgroup是方便多了,有点相当于是引用计数的功能,wg.Add(workers)就是把workers个计数加入到waitgroup中,之后wg.Done,就是执行wg.Add(-1),即是把引用计数减1,在最后的一步,执行wg.Wait()就是一直block在这里,直到wait group中的引用数目变成0。在测试goroutine的相关功能的时候,waitgroup的相关操作还是很好用的。

在stack over flow上面有一个很详细的介绍golang中锁机制使用的帖子。

通过阅读源代码我们可以知道sync.RWMutex是基于sync.Mutex实现的,其中的只读锁的实现使用类似引用计数的方式。

对于具体的全局锁,以及读锁,写锁的使用和区别,可以参考这个

相关参考

https://golang.org/ref/spec#Address_operators

https://code.google.com/p/go/issues/detail?id=3117

https://groups.google.com/forum/#!topic/golang-nuts/4_pabWnsMp0

https://golang.org/ref/spec

http://stackoverflow.com/questions/13101298/calling-a-pointer-method-on-a-struct-in-a-map

https://golang.org/ref/spec#Address_operators

http://stackoverflow.com/questions/16059038/can-not-assign-to-a-struct-member-from-map

http://stackoverflow.com/questions/36167200/how-safe-are-golang-maps-for-concurrent-read-write-operations

https://www.reddit.com/r/golang/comments/4a8y8c/go_16_fatal_error_concurrent_map_read_and_map/

权威参考 golang maps in action:
https://blog.golang.org/go-maps-in-action

推荐文章