计算机科学中最有用的数据结构之一是哈希表。 存在许多具有不同属性的哈希表实现,但通常它们提供快速查找,添加和删除。 Go提供了一个实现哈希表的内置Map类型。

1. 结构

  1. map[KeyType]ValueType

其中:
KeyType的值必须是任何可比较的类型,即可以用==比较的值

2. 声明和初始化

map是引用类型,类似的类型有切片和指针等,因此,可以通过声明一个未初始化的map来创建一个值为nill的map

  1. var m map[string]int

nill map不能用于存储键值对,否则会引起panic

  1. m = make(map[string]int, len)

其中,cap为选填值,该map类型数据的容量,超出容量会自动扩容

3. 映射散列表

go语言中的map实际上是一个哈希表,map中的数据被分散于一组桶(buckets)中,每个桶最多存储8个键值对,如果多于8个,则申请一个新的bucket,并将它与之前的bucket连接起来,形成链表。
bmap_chain.png
因此hmap的整体结构如下图所示
whole.png

对map进行存储,查找,删除等操作,就是对对应桶中的数据进行相应操作。go语言内置一个散列函数,用于将指定map的key转化为一个散列值(hash),散列值的低位用于标记该键值存储的桶(bucket)的标号,高位则用于标记对应桶中key的位置。

3.1 散列函数的工作流程:

图片来自 《Go语言实战》,第 93 页.png

3.2 go语言中的map结构:

图片来自 《Go语言实战》,第 92 页.png

3.3 查找指定key对应的值:

all_elem.png
每个桶中key/value的存储形式为:
key_value.png

4.增量扩容

如图3.3所示的哈希表增长时,go语言会将桶(bucket)数组的数量扩充一倍,产生一个新的bucket数组,并将旧数组中的数据迁移至新数组。
扩容是指添加一个bucket_x,每个bucket_x数组的容量为2个。

所以,map的key是无须的:

  • 在map扩容的过程中,原来在同一个桶中的key有可能被搬移到不同的桶中
  • 在遍历map时,并不是固定地从0号桶开始的,遍历起始桶是一个随机值。

另外一个引申点是:无法对map的key或value取地址

  • 原因还是在于map的存在扩容过程,map中的key和value在内存中的位置不是固定的,因此对map的key或value进行取地址操作会触发panic

5. 并发

map对于并发使用是不安全的,因此,当需要从同时执行的goroutine中读写map时,需要加入读写锁控制sync.RWMytex。

这是因为对map的查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。 另外sync.Map是线程安全的,可以全局使用。

例如:

  1. var counter = struct{
  2. sync.RWMutex
  3. m map[string]int
  4. }{m: make(map[string]int)}