由浅到深,入门Go语言Map实现原理
Go源码版本1.13.8
前言
写博客的目的?
把自己学习知识进行一个总结。同时把一些可能困难、复杂难以理解的东西自我消化吸收后,简单化输出,降低他人的学习成本,提高他人的学习效率,主要为如下两点:
- 自我学习的总结过程
- 简化他人学习成本的结果
其次,或多或少存在自己学习过程中理解不到位或者错误的地方希望大家可能及时帮我纠正,感谢以及说一声抱歉。
为什么博客更新的这么慢?
- 学习的难度在不断的增加,产出越来越慢
- 比以前懒了?
今天要分享的是主要内容是Go语言Map底层实现
,目的让大家快速了解Go语言Map
底层大致的实现原理。读完本篇文章你可以获得收益、以及我所期望你能获取的收益如下:
收益序号 | 收益描述 | 掌握程度 |
---|---|---|
收益1 | 大致对Go语言Map底层实现有一个了解 | 必须掌握 |
收益2 | 大致知道Go语言Map是如何读取数据的 | 必须掌握 |
收益3 | 熟悉Go语言Map底层核心结构体hmap |
可选 |
收益4 | 熟悉Go语言Map底层核心结构体bmap |
可选 |
收益5 | 熟悉Go语言Map底层里的溢出桶 | 可选 |
收益6 | 熟悉Go语言Map是如何读取数据的 | 可选 |
收益1和收益2是看了本篇文章希望大家必须掌握的知识点,其他的为可选项,如果你对此感兴趣或者已经掌握了收益1、2可以继续阅读此处的内容。
对于本篇文章的结构主要按如下顺序开展:
- 简单看看一般Map的实现思路
- Go语言里Map的实现思路(入门程度:包含收益1、2)
- Go语言里Map的实现思路(熟悉程度:包含收益3、4、5、6)
其次,本篇文章主要以Map的读来展开分析,因为读弄明白了,其他的写、更新、删除等基本操作基本都可以猜出来了,不是么😏。
简单看看一般Map的实现思路
直入主题,一般的Map会包含两个主要结构:
- 数组:数组里的值指向一个链表
- 链表:目的解决hash冲突的问题,并存放键值
大致结构如下:
读取一个key值的过程大致如下:
key |
接着我们来简单看看Go语言里Map的实现思路。
Go语言里Map的实现思路(入门程度)
包含收益1、2
Go语言解决hash冲突不是链表,实际主要用的数组(内存上的连续空间),如下图所示:
备注:后面我们会解释上面为啥用的“主要”两个字。 |
但是并不是只使用一个数组(连续内存空间)存放键和值,而是使用了两个数组分别存储键和值,图示如下:
上图中:
- 分别对应的是两个核心的结构体
hmap
和bmap
bmap
里有两个数组分别存放key和value
把上面简化的关系转换一下,其实就是这样的一个大致关系,如下图所示:
我们通过一次读操作
为例,看看读取某个key的值的一个大致过程:
步骤编号 | 描述 |
---|---|
① | 通过hash函数获取目标key的哈希,哈希和数组的长度通过位操作获取数组位置的索引(备注:获取索引值的方式一般有取模或位操作,位操作的性能好些) |
② | 遍历bmap里的键,和目标key对比获取key的索引(找不到则返回空值) |
③ | 根据key的索引通过计算偏移量,获取到对应value |
读过程图示如下:
这么看起来是不是“很简单”、很清晰,所以读到这里,你是不是已经入门了Go语言Map底层实现
并且:
- 大致对Go语言Map底层实现有一个了解(收益1)
- 大致知道Go语言Map是如何读取数据的(收益2)
然而实际情况不止如此,我们再稍微深入的探索下,有兴趣的可以继续往下看,没兴趣可以不用继续往下看了(开玩笑=^_^=),反正已经达到目的了,哈哈😏。
Go语言里Map的实现思路(熟悉程度)
包含收益3、4、5、6
想要深入学习,首先得了解下上面提到了实现Map的两个核心结构体hmap
和bmap
。
核心结构体hmap
收益3: 熟悉Go语言Map底层核心结构体`hmap` |
hmap
的结构其实刚开始看起来其实还是比较复杂的,有不少的字段,具体字段如下图所示:
字段释义如下:
字段 | 解释 |
---|---|
count | 键值对的数量 |
B | 2^B=len(buckets) |
hash0 | hash因子 |
buckets | 指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。如下图所示 |
oldbuckets | 扩容时,存放之前的buckets(Map扩容相关字段) |
extra | 溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对 |
noverflow | 溢出桶里bmap大致的数量 |
nevacuate | 分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段) |
flags | 状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段) |
备注:本次内容不涉及Map的扩容逻辑。 |
重点看一些字段的含义和用处。
字段buckets
buckets
指向了一个数组(连续的内存空间),数组的元素是bmap
类型,这个字段我们称之为正常桶。
hmap
的源码和地址如下:
// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go |
核心结构体bmap
收益4: Go语言Map底层核心结构体`bmap` |
正常桶hmap.buckets
的元素是一个bmap
结构。bmap
的具体字段如下图所示:
字段释义如下:
字段 | 解释 |
---|---|
topbits | 长度为8的数组,[]uint8,元素为:key获取的hash的高8位,遍历时对比使用,提高性能。如下图所示 |
keys | 长度为8的数组,[]keytype,元素为:具体的key值。如下图所示 |
elems | 长度为8的数组,[]elemtype,元素为:键值对的key对应的值。如下图所示 |
overflow | 指向的hmap.extra.overflow 溢出桶里的bmap ,上面的字段topbits 、keys 、elems 长度为8,最多存8组键值对,存满了就往指向的这个bmap 里存 |
pad | 对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件 |
推断出bmap
结构字段的代码和位置如下:
// https://github.com/golang/go/blob/go1.13.8/src/cmd/compile/internal/gc/reflect.go |
结论:每个
bmap
结构最多存放8组键值对。
hmap
和bmap
的基本结构合起来
分别了解了hmap
和bmap
的基本结构后,我们把上面的内容合并起来,就得到如下的Map结构图:
溢出桶
收益5: 熟悉Go语言Map底层里的溢出桶 |
上面讲bmap
的时候,我们不是得到了个结论么“每个bmap
结构最多存放8组键值对。”,所以问题来了:
正常桶里的
bmap
存满了怎么办?
解决这个问题我们就要说到hmap.extra
结构了,hmap.extra
是个结构体,结构图示和字段释义如下:
字段 | 解释 |
---|---|
overflow | 称之为溢出桶。和hmap.buckets 的类型一样也是数组[]bmap ,当正常桶bmap 存满了的时候就使用hmap.extra.overflow 的bmap 。所以这里有个问题正常桶hmap.buckets 里的bmap 是怎么关联上溢出桶hmap.extra.overflow 的bmap 呢?我们下面说。 |
oldoverflow | 扩容时存放之前的overflow(Map扩容相关字段) |
nextoverflow | 指向溢出桶里下一个可以使用的bmap |
源码和地址如下:
// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go |
问题:正常桶
hmap.buckets
里的bmap
是怎么关联上溢出桶hmap.extra.overflow
的bmap
呢?
答:就是我们介绍bmap
结构时里的bmap.overflow
字段(如下图所示)。bmap.overflow
是个指针类型,存放了对应使用的溢出桶hmap.extra.overflow
里的bmap
的地址。
问题又来了:
问题:正常桶
hmap.buckets
里的bmap
是什么时候关联上溢出桶hmap.extra.overflow
的bmap
呢?
答:Map写操作的时候。这里直接看关键代码:
// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go |
下面代码这段代码解释了,上面的源码中为何如此判断预分配溢出桶的bmap
是最后一个的原因。
// https://github.com/golang/go/blob/go1.13.8/src/runtime/map.go |
当hmap
存在溢出桶时,且当前溢出桶只被使用了一个bmap时,我们可以得到如下的关系图:
同时我们可以看出正常桶的bmap
和溢出桶的bmap
实际构成了链表关系,所以这也解释了开篇我们说到的“Go里面Map的实现主要用到了数组”,其次还用到了链表。
再次分析Map的读
收益6: 熟悉Go语言Map是如何读取数据的 |
通过上面的学习,我们再次通过一次读操作为例,看看读取某个key的值的一个大致过程:
结合代码分析下整个大体的过程:
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { |
结语
原创不易,年底了,征一波稿费,1元就行,在此谢过大家。
参考: |