go 数据结构map

要理解 map , 先要理解哈希表

哈希是一类算法(映射算法,有很多种),类似一元一次方程那种,比如取余。

而哈希表的结构一种实现方式是一种存储链表的数组。

首先他提供哈希函数创建数组(比如哈希算法是对7取余,则创建一个长度为7的存储链表的数组)。

当有数据需要存储的时候,先通过哈希函数确定应该存储到哪个数组下面。

但是数组的长度是有限的,而可能存储进来的数据是无限的, 那肯定就会出现不同的数据哈希结果相同的情况(哈希碰撞)。

为了解决这个问题,每个哈希表里数组的成员对应的都是一个链表,当出现重复的数据时, 就往链表中添加节点。

在查询的时候,先对需要查询的key进行取模,找到数组中对应的链表, 然后再遍历链表。

由上面的规则可以看出, 初始数组分配越大,则查询的时候越快,但是容易造成内存浪费,

相反, 如果数组分配的越小,则数据多的时候链表越长,查询越慢,但是被浪费的内存少。

所以在设计哈希表的时候,会考虑数组的扩容,流程大致如下:

1、先生成一个较小的存储链表的数组

2、等数据到达一定量的时候更换哈希算法,然后生成一个新的大点的存储链表的数组

3、将旧链表数组中的数据迁移到新的链表数组中(具体的迁移时间看情况,有些是在查询到某个key的时候才迁移这个key)

哈希表结构图大致如下 :

https://upload-images.jianshu.io/upload_images/6534448-9dc8e65a49c2d619.png?imageMogr2/auto-orient/strip|imageView2/2/w/541/format/webp

go语言中定义map常量 go中map存储_数组

而我们的 map 可以理解为一种哈希表,使用拉链的方式消除哈希碰撞。

基于上面的认知, go 的map有两种重要的数据结构,hmap和bmap。

其中 hmap 充当了哈希表中数组的角色, bmap充当了链表的角色。

hmap中包含一个指向bmap数组的指针, key 经过哈希函数之后得到一个数, 这个数低位用于选择 bmap(当作bmap数组指针的下表), 高位用于放在bmap的[8]uint8数组中,用于快速试错。 然后一个bmap可以指向下一个bmap(拉链)。

go语言中定义map常量 go中map存储_go语言中定义map常量_02

hmap如上图。

go 的 map 中用于存储的结构是 bucket(即bmap)数组。而bucket结构如下:

go语言中定义map常量 go中map存储_数据_03

他的核心部分是中间部分,我们使用的 map 中的 key和value都是存储在这里。

高位哈希值数组中记录的是当前 bucket 中 key相关的“索引”(有了这个就不用再遍历链表?)

另外一个字段就是指针。

go的map中也有应该哈希函数,哈希函数会把求得的值按照用途一分为二,高位和低位。

红色为低位,用于查找当前key属于 hmap 中的哪个bucket(链表),蓝色为高位, 用于定位bucket中的key。

【需要特别注意的一点是】,我们map中的key、value值都是存在同一个数组中的,数组的顺序是这样的:

go语言中定义map常量 go中map存储_链表_04

并不是key0/value0/key1/value1的形式, 这样主要是为了消除内存对齐带来的空间浪费。

所以go里面的 map 整体结构应该如下 :

go语言中定义map常量 go中map存储_数组_05

go里面的map同样存在扩容的需求, 他的实现方式如下:

当哈希表增长到一定程度的时候,go会将hmap中的bucket数组的数量扩充一倍,产生一个新的bucket数组,并将旧数组的数据迁移至新数组。

其中扩充的判断条件就是哈希表中的加载因子。

【需要注意的是】:在扩容迁移时,并不是立刻把旧的数组中的元素转移到新的bucket中,而是只有当访问到某个具体的bucket时,才会把bucket中的数据迁移到新的bucket中。

【注意】:这里并不会直接删除旧的bucket,而是把原来的引用去掉,利用GC清除内存。

删除map中的数据时,找到了map中的数据之后,针对key和value分别做如下操作:

1、如果``key``是一个指针类型的,则直接将其置为空,等待GC清除;

2、如果是值类型的,则清除相关内存。

3、同理,对``value``做相同的操作。

4、最后把key对应的高位值对应的数组index置为空。