//这就是map的结构体,可以在src/runtime包下找到map.go
type hmap struct {
count int // map中元素的个数,所以len(map)可以O(1)获取
flags uint8 //标记该map的状态,后续讲到说明
B uint8 // 2^B就是桶的个数
noverflow uint16 // 溢出桶的数量
hash0 uint32 // hash种子
buckets unsafe.Pointer // 指向桶数组的指针
oldbuckets unsafe.Pointer // 老的桶数组指针?当然是扩容时用的啦
nevacuate uintptr // 扩容进度
extra *mapextra // 一些额外的字段,太多了,后面再讲
}
我们先来讲讲 unsafe.Pointer ,包名unsafe就可以看出官方并不推荐使用,虽然很多源码都用到了它。
它跟c语言的void* 类似,可以接收任意类型的指针,也可以和uintptr(存的也是地址值,但无法通过uintotr访问内存)相互转化。
源码也非常简单:
type ArbitraryType int
type Pointer *ArbitraryType//表面上看,其实就是一个int类型的指针,但不管啥类型的指针其实都是地址值
接下来我们看看创建一个map时发生了什么,指针最后又指向了哪里?
这时候去看make函数的源码就会发现:
func make(t Type, size ...IntegerType) Type//make函数只有这短短的一行,也没找到它的任何实现
百度之后发现,原来编译器‘偷偷’用makemap函数替换掉了它(仅创建map的情况),那就看看makemap函数吧(其实过程有点复杂的,但是makemap是最核心的代码)
func makemap(t *maptype, hint int, h *hmap) *hmap {
//检查是否会出现内存不足或超出能分配内存的最大值
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化map,就是调用new函数创建一个hmap结构体
if h == nil {
h = new(hmap)
}
//生成一个随机的hash种子
h.hash0 = fastrand()
//确定B的值
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 对map进行一些初始化
// 创建一个bucketArray数组,如果B>=4,还会额外创建溢出桶,放到extra里面
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
看到这里眼尖的早已发现,原来桶是一个叫bmap的东西,下面是它的源码
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
很简单的结构体,只有一个字段?那当然是不可能的!go在编译过程中动态的给它添加了一些字段,让我们来看看它的完全体:
type bmap struct {
tophash [bucketCnt]uint8 //长度为8的数组,存key的高8位
keys [8]keytype //长度为8的数组,存的key
values [8]valuetype //长度为8的数组, 存的是value
pad uintptr //对齐字段时使用的
overflow uintptr //溢出桶位置
}
注意一点是,key和value是分开存储的,官方解释是在避免某些情况下的字段对齐引起的内存浪费。
再讲回extra字段,它是一个mapextra类型的指针,结构如下:
type mapextra struct {
overflow *[]*bmap//溢出桶指针
oldoverflow *[]*bmap//旧的溢出桶,扩容时使用
nextOverflow *bmap //下一个溢出桶
}
除外,源码中有一段注释也值得一看:
英文原版:
if both key and elem do not contain pointers and are inline, then we mark bucket type as containing no pointers. This avoids scanning such maps. However, bmap.overflow is a pointer. In order to keep overflow buckets alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow. overflow and oldoverflow are only used if key and elem do not contain pointers. overflow contains overflow buckets for hmap.buckets. oldoverflow contains overflow buckets for hmap.oldbuckets. The indirection allows to store a pointer to the slice in hiter.
三毛钱翻译加五毛钱简化:
当key和value不包含指针且可以被inline(<=128字节)时被使用,会对桶数组进行标记,避免GC时的全表扫描。其中说到overflow只有在key和value无指针时使用,指的是extra里的字段,并非不使用溢出桶
最后的最后,来张清晰的结构图