[TOC]git
笼统的来讲,go的map底层是一个hash表,经过键值对进行映射。 键经过哈希函数生成哈希值,而后go底层的map数据结构就存储相应的hash值,进行索引,最终是在底层使用的数组存储key,和value。稍微详细的说,就设计到go map 的结构。hmap 和bmap。github
哈希表就不得不说hash函数。hash函数,有加密型和非加密型。加密型的通常用于加密数据、数字摘要等,典型表明就是md五、sha一、sha25六、aes256这种;非加密型的通常就是查找。在map的应用场景中,用的是查找。选择hash函数主要考察的是两点:性能、碰撞几率。golang使用的hash算法根据硬件选择,若是cpu支持aes,那么使用aes hash,不然使用memhash,memhash是参考xxhash、cityhash实现的,性能很是好。golang
具体hash函数的性能比较能够看:http://aras-p.info/blog/2016/...算法
哈希函数会将传入的key值进行哈希运算,获得一个惟一的值。go语言把生成的哈希值一分为二,好比一个key通过哈希函数,生成的哈希值为:8423452987653321
,go语言会这它拆分为84234529
,和87653321
。那么,前半部分就叫作高位哈希值,后半部分就叫作低位哈希值。后面会说高位哈希值和低位哈希值是作什么用的。数组
高位哈希值:是用来肯定当前的bucket(桶)有没有所存储的数据的。数据结构
低位哈希值:是用来肯定,当前的数据存在了哪一个bucket(桶)函数
// Go map的一个header结构 type hmap struct { count int // map的大小. len()函数就取的这个值 flags uint8 //map状态标识 B uint8 // 能够最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子即:map长度=6.5*2^B //B能够理解为buckets已扩容的次数 noverflow uint16 // 溢出buckets的数量 hash0 uint32 // hash 种子 buckets unsafe.Pointer //指向最大2^B个Buckets数组的指针. count==0时为nil. oldbuckets unsafe.Pointer //指向扩容以前的buckets数组,而且容量是如今一半.不增加就为nil nevacuate uintptr // 搬迁进度,小于nevacuate的已经搬迁 extra *mapextra // 可选字段,额外信息 }
hmap是map的最外层的一个数据结构,包括了map的各类基础信息、如大小、bucket。首先说一下,buckets这个参数,它存储的是指向buckets数组的一个指针,当bucket(桶为0时)为nil。咱们能够理解为,hmap指向了一个空bucket数组,而且当bucket数组须要扩容时,它会开辟一倍的内存空间,而且会渐进式的把原数组拷贝,即用到旧数组的时候就拷贝到新数组。性能
// Go map 的 buckets结构 type bmap struct { // 每一个元素hash值的高8位,若是tophash[0] < minTopHash,表示这个桶的搬迁状态 tophash [bucketCnt]uint8 // 第二个是8个key、8个value,可是咱们不能直接看到;为了优化对齐,go采用了key放在一块儿,value放在一块儿的存储方式, // 第三个是溢出时,下一个溢出桶的地址 }
bucket(桶),每个bucket最多放8个key和value,最后由一个overflow字段指向下一个bmap,注意key、value、overflow字段都不显示定义,而是经过maptype计算偏移获取的。测试
bucket这三部份内容决定了它是怎么工做的:优化
bucket的设计细节:
在golang map中出现冲突时,不是每个key都申请一个结构经过链表串起来,而是以bmap为最小粒度挂载,一个bmap能够放8个key和value。这样减小对象数量,减轻管理内存的负担,利于gc。
若是插入时,bmap中key超过8,那么就会申请一个新的bmap(overflow bucket)挂在这个bmap的后面造成链表,优先用预分配的overflow bucket,若是预分配的用完了,那么就malloc一个挂上去。注意golang的map不会shrink,内存只会越用越多,overflow bucket中的key全删了也不会释放
如图所示:
简单结构为图:
工做流程:
查找或者操做map时,首先key通过hash函数生成hash值,经过哈希值的低8位来判断当前数据属于哪一个桶(bucket),找到bucket之后,经过哈希值的高八位与bucket存储的高位哈希值循环比对,若是相同就比较刚才找到的底层数组的key值,若是key相同,取出value。若是高八位hash值在此bucket没有,或者有,可是key不相同,就去链表中下一个溢出bucket中查找,直到查找到链表的末尾。
碰撞冲突:若是不一样的key定位到了统一bucket或者生成了同一hash,就产生冲突。 go是经过链表法来解决冲突的。好比一个高八位的hash值和已经存入的hash值相同,而且此bucket存的8个键值对已经满了,或者后面已经挂了好几个bucket了。那么这时候要存这个值就先比对key,key确定不相同啊,那就今后位置一直沿着链表日后找,找到一个空位置,存入它。因此这种状况,两个相同的hash值高8位是存在不一样bucket中的。
查的时候也是比对hash值和key 沿着链表把它查出来。 还有一种状况,就是目前就 1个bucket,而且8个key-value的数组尚未存满,这个时候再比较完key不相同的时候,一样是沿着当前bucket数组中的内存空间日后找,找到第一个空位,插入它。这个就至关因而用寻址法来解决冲突,查找的时候,也是先比较hash值,再比较key,而后沿着当前内存地址日后找。
go语言的map经过数组+链表的方式实现了hash表,同时分散各个桶,使用链表法+bucket内部的寻址法解决了碰撞冲突,也提升了效率。由于即便链表很长了,go会根据装载因子,去扩容整个bucket数组,因此下面就要看下扩容。
这篇文章,只是对map底层的结构进行了说明,具体建立、查找、删除等的流程是差很少,可是具体的细节仍是有不少。因此,我就不一一写出了,贴一下其余博主写的文章,很详细。
5.1 剖析golang map的实现
地址:https://www.jianshu.com/p/092...
5.2 Golang map 的底层实现
地址:https://www.jianshu.com/p/aa0...
重点来了,必须看:很是详细的map源码说明
https://github.com/cch123/golang-notes/blob/master/map.md
参考:
https://www.jianshu.com/p/092...