Map

map[K]V

1. 使用要点

Map 使用前必须初始化 m := make(map[K]V) 。zero value 是 nil,零值 map 可读不可写,写就 panic。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    ...
	var m map[string]struct{}
	fmt.Println(m == nil) // true, m is nil

	v := m["a"]    // reading from nil map is ok
	fmt.Println(v) // {}

	m["a"] = struct{}{} // panic: assignment to entry in nil map
	fmt.Println(v)
    ...

Key 类型 K 必须是可比较类型(comparable),即必须可受作用于 ==!= 操作符,slicemapfunction 都不行。Value 类型 V 为任意类型 any。

Map[K] 返回结果可以是一个值,也可以是两个值。如果获取一个不存在的 key 对应的值时,会返回零值。为了区分真正的零值和 key 不存在这两种情况,可以根据第二个返回值来区分。

1
2
3
4
5
6
7
8
9
    ...
	m := make(map[string]struct{})

	v1 := m["a"]
	fmt.Println(v1) // {}

	v2, exist := m["a"]
	fmt.Println(v2, exist) // {} false
    ...

Map 是无序的。所以当遍历一个 map 对象的时候,无法保证两次遍历元素的顺序是一样的,也不能保证和插入的顺序一致。想要按照 key 的顺序获取 map 的值,需要先取出所有的 key 进行排序,然后按照这个排序的 key 依次获取对应的值。而如果我们想要保证元素有序,比如按照元素插入的顺序进行遍历,可以使用辅助的数据结构,比如 orderedmap,来记录插入顺序。

Map 本身非并发安全,遇并发读写需加锁。加之大多符合读多写少的应用场景,所以一般使用 RWMutex。

2. 安全与性能

用锁去同步多个并发执行体是常用的同步手段。为了保证有锁代码的性能,锁的使用原则就是:尽量减少锁的粒度和锁的持有时间

我们在业务代码中要尽可能保证临界区内不做耗时操作,这样可以减少锁的持有时间。减少锁的粒度常用方法是分片(shard),把一把锁分成几把锁,每个锁控制一个分片。Go 比较知名的分片并发 map 的实现是 orcaman/concurrent-map

3. sync.Map

Go 1.9 增加了线程安全的 map 即 sync.Map,但是它并不是用来替换内建 map 类型的。 官方文档指出,在以下两个场景中使用 sync.Map,会比使用 map + RWMutex 的方式,性能要好得多:

  1. 只会增长的缓存系统中,一个 key 只写入一次而被读很多次;
  2. 多个 goroutine 为不相交的键集读、写和重写键值对。

这两个场景说得都比较笼统,而且,这些场景中还包含了一些特殊的情况。所以,官方建议你针对自己的场景做性能评测,如果确实能够显著提高性能,再使用 sync.Map。