结构体中一个 map 字段,函数调用传递下去后,并发将结构体作为参数调用,而后 panic了,原因很简单,并发读写了~难得浮生片刻闲,好好解读下 go 中 map。笔者
本文所示源码基于 go1.17 版本。如果想理解,得多一些耐心,不过阅读完以后还是有些收获。
Python 类似结构叫 dict,貌似高级点的语言都有这种结构,怎么去理解 map 呢?看一下 wiki map,不做过多赘述,普遍认知就是 “哈希表(hash table)”,首先看 Go 中 map 是怎么定义。
Go map 初始化使用
var a, b map[int]int
a = make(map[int]int, 5)
这中 a 可以进行map 的元素操作了,b则不可以,b还是 nil。原因 map 的 零值是 nil,可是为啥呢?
这个得从map这种结构底层存储看下,我们打开 runtime/map.go
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
...
}
可见,make map 返回的底层结构就是 *hmap
,所以声明map时候,指针的零值是 nil。我们先了解下 hmap
结构体的字段,这个就是 map结构的底层存储,所有map 的增删改查都是围绕着 hamp 这个结构体的操作。
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
字段 | 说明 |
---|---|
count | map当前元素个数,平时 len(mapObject) 得到的值 |
flags | 标记,对应 const 中 // flags 下的几个状态 |
B | buckets 的数量的log2 值,对应 buckets = 2^B |
noverflow | overflow 的 bukects 的数量 |
buckets | 桶数组,用来存储实际 key-value 的单元 |
oldbuckets | 旧桶数组,map 出现扩容时会用到 |
nevacuate | 扩容已搬迁的桶数量,比此数低的 bucket 都已完成了扩容搬迁 |
hmap
内部另一个核心结构就是 bucket
,实际 hash table 的元素落入单元 – bucket【桶】
,bucket结构呢?
// A bucket for a Go map.
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.
}
实际运行时会对 bmap
结构动态改变,最终的结构会是:
type bmap struct {
tophash [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
可以参考:https://www.infoq.cn/article/occ9isefi4pwcxxmifrx。bmap
中是有 8个 Cell 的,可以存放 8 组 key-value
整个哈希表的结构基本就有了,哈希表解决哈希冲突基本就是两类方法,开放地址 or 拉链,Go hmap就是在 bmap 上拉链解决哈希冲突。
借用 Google 图库的 Go map 结构图:
由上面结构再仔细看下 bmap
的字段
字段 | 说明 |
---|---|
tophash | 当前桶中数据的 hash 值的高8位组成的长度为 8 的数组 |
keys | 存放对应 tophash 数组的 key |
values | 存放对应 tophash 数组的 value |
overflow | overflow 的 bmap |
这两个底层的结构是理解 map 后续操作的基础,可以用 dlv
工具调试一个 map
数据如下图:
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 根据 map 指定容量估算 map 所需内存,如果溢出或者内存超过最大限制把 map 容量置 0
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 如果当前 hmap 为空,则new hmap,并且设置此 hmap 的哈希计算种子随机数 hash0
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// 根据负载因子计算合理 B 值,LoadFactor 是用来衡量 map 容量和当前元素数量之间的关系的一个参数
// 当其超越某个界值表示map需要进行扩容,后文详解。持续对 B 进行负载因子计算,得出B合理的值
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// B值对应的 bucket 的数量为 2^B,对 hmap 的 桶进行分配初始化
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
}
计算 key 的 hash 值,64位机器得到一个 长度为 64bit 的二进制码,其中 低 B
位用来确定落在 hmap
的哪个 bucket
中(前文已说明 hmap
的 bucket
个数为 2^B )
而后在 bmap
中,定义一个 key 用的是 key 的 hash 值 高8位,bmap
结构的 tophash
就是有 8个 8bit 的组成
由上面 map key 定位方式可知,读取 key 的过程就是先 定位 bucket,再在 bmap 中对逐个比较,包括 bmap
后接的 overflow 的 bmap
,看下源码
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// build 时候开启 race 时会记录检测
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
// build 时候开启内存消毒器互操作
if msanenabled && h != nil {
msanread(key, t.key.size)
}
// 真正获取key的操作开始
// 判定当前 map 是否为 nil or 空,则默认返回对应 value 的 0 值
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
// 如果 h.flags 做正在进行写操作的比对
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 利用h初始化时确定 h.hash0计算key 的hash值,并且取 低 B 位作 bucket 的偏移量
hash := t.hasher(key, uintptr(h.hash0))
m := bucketMask(h.B)
// 通过 h.buckets 和 偏移量(hash&m)*单个(bucket)大小得出当前 bucket - bmap
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 可能当前map正处在扩容搬迁的流程,确定是否需要去 oldbuckets 中定位 bucket,暂且不考虑这段逻辑,就当 map 是不涉及扩容逻辑
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
// 计算当前key高8位 hash值,值得注意其中有一些保留值,所以会对高 8 位做最小minTopHash 判定运算
top := tophash(hash)
// 开启双层遍历,外层会遍历 bmap 以及 bmap后链的 overflow 的 bmap
bucketloop:
for ; b != nil; b = b.overflow(t) {
// bmap 逐个 cell 遍历,即 8 个 tophash 的
for i := uintptr(0); i < bucketCnt; i++ {
// 当前 cell top值不匹配
if b.tophash[i] != top {
// 判定特殊标记,是否是一个全空 bmap 如果是则直接终止循环
if b.tophash[i] == emptyRest {
break bucketloop // 终止整个双层循环
}
continue
}
// 通过bmap地址 + dataOffset段偏移量 + key偏移量i * key大小,定位到k
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
// 如果k是指针,对 k 解引用
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 对 k 和 寻找对象 key 进行类型的 equal 等值判定,如果相等则代表在 map 中寻找到了 key
if t.key.equal(key, k) {
// 寻找 value 通过
// bmap地址 + dataOffset段偏移量 + 8个key偏移量 + i号value相对偏移量,定位到 value
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
// 如果 value 也是指针则解引用
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
其中 dataOffset 是一个常量
// data offset should be the size of the bmap struct, but needs to be
// aligned correctly. For amd64p32 this means 64-bit alignment
// even though pointers are 32 bit.
dataOffset = unsafe.Offsetof(struct {
b bmap
v int64
}{}.v)
在理解 map 代码时对于给定的 常量含义需要有一定了解,否则阅读代码不太明白判定,譬如 emptyRest
核心就是为了加速遍历 bucket
理解了map的构造和 key 的定位去阅读 map 的 access 还是比较容易。Go 对于 map access 提供了很多方法,还有一个 mapaccess2
对应我们在使用map时附加获取存在判定
// 底层调用 mapaccess2
a, exist := targetMap["my_key"]
写操作是相对复杂,其中包括在了正常key的写入,并且在 map 容量不够时触发扩容,先忽略扩容,只看正常的写入。map的写入实现在mapassign
中,详情如下:
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 对于没有make 初始化的,还是nil的 map写操作直接抛出 panic
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
// 类似 access
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.key.size)
}
// 当前map 是否有正在执行写操作的,如果有已经在写的,则出现了并发写,直接异常退出
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 计算当前 assigen key hash值
hash := t.hasher(key, uintptr(h.hash0))
// Set hashWriting after calling t.hasher, since t.hasher may panic,
// in which case we have not actually done a write.
h.flags ^= hashWriting // map flag 置为"hashWriting"标记,对应前面的判定
// 如果make map 没有指定容量,初开始map 的buckets数组是空,此处初始化h.buckets
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 利用B值,确定当前需要写入的 bucket 编号
bucket := hash & bucketMask(h.B)
if h.growing() { // 判定map是否在扩容,如果在扩容开始执行扩容的数据搬迁,后续详细介绍
growWork(t, h, bucket)
}
// 计算具体 bucket 对象地址 bmap
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
top := tophash(hash) // key 高8位,后续写入b对应bmap的tophash数组中的value值即为 top
var inserti *uint8 // 需要写入tophash 数组的index
var insertk unsafe.Pointer // 写入 key 对象指针
var elem unsafe.Pointer // 写入key 对应 elem 对象指针
// 开启双层遍历,外层遍历 bukect到overflow bucket,内部遍历每个bucket cell,直到找到第一个非空的cell
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top { // 当前 cell 与 key 的top值不同
if isEmpty(b.tophash[i]) && inserti == nil { // 当前cell以为空标记且尚未确定 inserti
// 开始设置 inserti, insertk,elem,以当前i为偏移量计算对应存储地址
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
if b.tophash[i] == emptyRest { // map扩容搬迁逻辑,暂时忽略
break bucketloop
}
continue
}
// 取出当前i位置的k
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() { // 如果map的key是指针,则还原该指针对象
k = *((*unsafe.Pointer)(k))
}
// 判定当前k与需要assign的key 是否一致,如果不一致,说明当前 tophash 的高8位值与另一个k冲突了,需要继续循环,找寻可以插入当前key 的 bucket及i
if !t.key.equal(key, k) {
continue
}
// already have a mapping for key. Update it.
if t.needkeyupdate() { // 需要用 key 覆盖 k
typedmemmove(t.key, k, key)
}
// 循环开始条件没命中,i符合条件,对 elem计算地址值
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done // 寻找对应value放入的元素elem结束,转入done处执行
}
// 如果 bucket 没有找到,寻找当前bucket的overflow bucket
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// Did not find mapping for key. Allocate new cell & add entry.
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
if inserti == nil {
// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// store new key/elem at insert position
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = vmem
}
typedmemmove(t.key, insertk, key)
*inserti = top
h.count++
done:
// 再次对flags和hashWriting进行标志判定,如果与hashWriting不一致则退出程序
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// flags 对 hashWriting 按位置0,"&^" 表示按右边的hashWriting 二进制,为1的位置,置0
h.flags &^= hashWriting
if t.indirectelem() { // 如果 elem 是指针对象,解对象
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
elem 地址return 外层后,会进行value 的赋值操作。
这中间留意 flags 的置位和判定操作
上述的代码解析,暂且搁置了 扩容的阶段判定。主体map写入,就是上述路径。
中间有很多逻辑值得反复思考:
极致情况,如果 hmap 的 B=0,则 bucket 的数组长度为1,那么持续装入元素则变成 bucket后续接overflow bucket,再接overflow bucket,退化成了数组。失去了 map 的寻取特性。
golang中如何衡量一个 map 的容量?这其中有一个参数 loadFactor -- 负载因子
loadFactor 计算方式 loadFactor = count / (2^B)
默认值 loadFactorNum/loadFactorDen = 13/2 = 6.5
默认值是 6.5
// 当 map 不在扩容中,并且后续两个条件满足其一即扩容,这两个条件的含义
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
// overLoadFactor reports whether count items placed in 1<
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
// overflow bucket数量太多判定,当 B < 15 时,overflow 的 bucket > 2^B
// 当 B >= 15 时,overflow 的 bucket > 2^15
// 这两种都算是 overflow bucket 过多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// If the threshold is too low, we do extraneous work.
// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
// "too many" means (approximately) as many overflow buckets as regular buckets.
// See incrnoverflow for more details.
if B > 15 {
B = 15
}
// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}
对应上面两种判定,也会有两种扩容逻辑,如果是 loadFactor 超阈值,则说明 hmap 需要增加 buckets 的长度,但是buckets增加伴随着 B的变大,对应key hash值的低位长度变长,值也会发生变化,那么原来的 buckets 中的 value会有一个区分高低位搬移的过程,
另外的 overflow buckets过多,则不需要增加buckets数量,但是需要把原来的bmap 变的更紧凑,避免过多 overflow buckets,需要同位置bucket紧凑化搬移的过程,因为 B 没有变化
func hashGrow(t *maptype, h *hmap) {
// If we've hit the load factor, get bigger.
// Otherwise, there are too many overflow buckets,
// so keep the same number of buckets and "grow" laterally.
bigger := uint8(1) // 默认设置需要更多buckets
if !overLoadFactor(h.count+1, h.B) { // 如果不是因为 loadFactor 过高导致扩容,则不需要 bigger,并且hmap 标记为同尺寸扩容
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets // 将 bucket留到 oldbuckets
// 生成新的 bucket,bigger为0,则 len(newbuckets) == len(oldbuckets),否则len(newbuckets) = 2len(oldbuckets)
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
// 将 h.flags的 iterator | oldIterator 置0取值得flags
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 { // h正在迭代
flags |= oldIterator // 将 flags的 oldIterator 置位
}
// commit the grow (atomic wrt gc)
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
// 对 extra中的 overflow 和 oldoverflow 调整,将原先的overflow 调整为 oldoverflow
if h.extra != nil && h.extra.overflow != nil {
// Promote current overflow buckets to the old generation.
if h.extra.oldoverflow != nil {
throw("oldoverflow is not nil")
}
h.extra.oldoverflow = h.extra.overflow
h.extra.overflow = nil
}
// 新生成的 nextOverflow 不为 nil 时,设置 h.extra.nextOverflow
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}
// the actual copying of the hash table data is done incrementally
// by growWork() and evacuate().
}
整个上述的扩容逻辑是完成了额map容量的调整,并未对数据调整,容量调整的核心在 makeBucketArray 中
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
// 计算本次基础容量长度,2^b,并且 nbuckets 设置为 base
base := bucketShift(b)
nbuckets := base
// For small b, overflow buckets are unlikely.
// Avoid the overhead of the calculation.
if b >= 4 { // 对于小 map,没有必要设置overflow buckets,而所谓的 "小",就是 b < 4
// Add on the estimated number of overflow buckets
// required to insert the median number of elements
// used with this value of b.
nbuckets += bucketShift(b - 4) // 计算当前需要总 nbuckets 的数量
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz {
nbuckets = up / t.bucket.size
}
}
if dirtyalloc == nil { // 开启分配新的 nbuckets 长度 buckets
buckets = newarray(t.bucket, int(nbuckets))
} else {
// dirtyalloc was previously generated by
// the above newarray(t.bucket, int(nbuckets))
// but may not be empty.
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.ptrdata != 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
// 当 base != nbuckets 说明此时map的 nbuckets 是大于 2^b,多出来的则是 nextOverflow,条件段中计算 nextOverflow 的起点
if base != nbuckets {
// We preallocated some overflow buckets.
// To keep the overhead of tracking these overflow buckets to a minimum,
// we use the convention that if a preallocated overflow bucket's overflow
// pointer is nil, then there are more available by bumping the pointer.
// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}
所以,可以看出实际中 map的 buckets 和 overflow bucket 是在一个 bmap 的数组中的,对于hmap的buckets只会用到前面的 2^B 个,后续的是 overflow bucket,并且一个小map是没有 overflow bucket。因为 map 扩容首先进行的是上面的 hmap 的容量拓展,而另一个大开销在于值的迁移,这是一个逐步完成的过程。
上文 hashGrow
最后注释也说明map的迁移是在 growWork() and evacuate()
中,回归 map 的赋值会判定 h 是否正在 growing,相应会进行搬迁工作。
func growWork(t *maptype, h *hmap, bucket uintptr) {
// make sure we evacuate the oldbucket corresponding
// to the bucket we're about to use
evacuate(t, h, bucket&h.oldbucketmask()) // 实际执行对 bucket号的bucket搬迁
// evacuate one more oldbucket to make progress on growing
if h.growing() {
evacuate(t, h, h.nevacuate) 对 h.nevacuate 号 bucket 搬迁
}
}
最核心的 evacuate 操作,比较复杂,一切基础都是对 map 结构的理解,得铭记上面描述的 map 结构。
假设我们现在 hmap,B=5,则 hmap 的 buckets 长度为 32,如果进行负载因子过载过扩容,这容量翻倍,B=6,buckets 长度为 64。那么同样一个 key 的 hash值低
在原始的 5位值 10011
现在多 拓展一位,?10011
,对于这其中的值,如果
?
为0,则对应搬迁还是把旧的 10011
号搬到 B=6 时新长度为 64 的 buckets 的 010011
?
为1,则对应搬迁还是把旧的 10011
号搬到 B=6 时新长度为 64 的 buckets 的 110011
,原始编号高位偏移 2^5,就是 旧B 的掩码搬迁10011
号的 bucket 下所有元素还在这个编号下。再阅读源码
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 定位旧的编号为 oldbucket的bmap
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
newbit := h.noldbuckets() // 计算 oldbucket 的长度或者说 新B 的最高位掩码
if !evacuated(b) { // 如果 b 没有搬迁完开启搬迁
// TODO: reuse overflow buckets instead of using new ones, if there
// is no iterator using the old buckets. (If !oldIterator.)
// xy contains the x and y (low and high) evacuation destinations.
// 设置高低位搬迁的数组,低位搬迁就是对应说明的同编号搬迁,高位就是偏移 旧B 个间隔搬迁,先对低位 x 初始化
var xy [2]evacDst
x := &xy[0]
// 新的hmap 的 buckets 中旧 oldbucket 号 bmap
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
// 计算新的 bmap中 key 和 elem
x.k = add(unsafe.Pointer(x.b), dataOffset)
x.e = add(x.k, bucketCnt*uintptr(t.keysize))
// 如果不是同尺寸扩容,那么就是可能需要高位搬迁,对高位 y 的 bmap 初始化
if !h.sameSizeGrow() {
// Only calculate y pointers if we're growing bigger.
// Otherwise GC can see bad pointers.
y := &xy[1]
y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
y.k = add(unsafe.Pointer(y.b), dataOffset)
y.e = add(y.k, bucketCnt*uintptr(t.keysize))
}
// 开始双循环对 b 完整搬迁,外层从bmap到 bmap后的 overflow bmap 逐个扫,内层每个 bmap 内的 cell 逐个扫
for ; b != nil; b = b.overflow(t) {
// 分配待搬迁的 key,value的elem
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
top := b.tophash[i] // bmap当前 i 中的 top值
if isEmpty(top) { // 如果是空值
b.tophash[i] = evacuatedEmpty // 设置该i号cell 已搬迁 继续下一个cell
continue
}
if top < minTopHash { // 如果top比最小hash值小,说明map出现了未知错误,直接退出程序
throw("bad map state")
}
// 对 key 进行是否是指针引用的判定和解释
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
if !h.sameSizeGrow() { // 如果不是同尺寸扩容,要判定是否高位搬迁
// Compute hash to make our evacuation decision (whether we need
// to send this key/elem to bucket x or bucket y).
hash := t.hasher(k2, uintptr(h.hash0)) // 计算i号cell中 key 的 hash 值
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
// If key != key (NaNs), then the hash could be (and probably
// will be) entirely different from the old hash. Moreover,
// it isn't reproducible. Reproducibility is required in the
// presence of iterators, as our evacuation decision must
// match whatever decision the iterator made.
// Fortunately, we have the freedom to send these keys either
// way. Also, tophash is meaningless for these kinds of keys.
// We let the low bit of tophash drive the evacuation decision.
// We recompute a new random tophash for the next level so
// these keys will get evenly distributed across all buckets
// after multiple grows.
// 这段选择将数据搬迁使用高位还是低位的逻辑比较特殊,因为对于 NaN,每次计算 key 的hash值不一致,所以,这部分就根据 top的最低位判定,可以忽略
useY = top & 1
top = tophash(hash)
} else {
// 正常判定是否进行高位搬迁,应该用 新B 的高位
if hash&newbit != 0 {
useY = 1
}
}
}
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}
// 标记当前 旧b 的i号 cell 是搬迁去新的 evacuatedX or evacuatedY
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
// 设置使用 x 搬迁还是 y 作为目标搬迁地址
dst := &xy[useY] // evacuation destination
// 如果目标地已经满了,对目标bmap拓展 overflow bmap
if dst.i == bucketCnt {
dst.b = h.newoverflow(t, dst.b)
dst.i = 0
dst.k = add(unsafe.Pointer(dst.b), dataOffset)
dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
}
// 开始对目标新的 bmap cell 填充 top值,key,elem
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
typedmemmove(t.key, dst.k, k) // copy elem
}
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
// 目标bmap的i,key,elem 后移
dst.i++
// These updates might push these pointers past the end of the
// key or elem arrays. That's ok, as we have the overflow pointer
// at the end of the bucket to protect against pointing past the
// end of the bucket.
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// 旧 buckek 变迁完了,且bucket 是指针类型数据,则清理旧 bucket,帮助 GC 回收内存
// Unlink the overflow buckets & clear key/elem to help GC.
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// Preserve b.tophash because the evacuation
// state is maintained there.
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
// 如果 oldbucket 和 h中需要evacuate的bucket编号一致,那再计算出下一个待搬迁的 bucket 编号,每个 growWork 会进行 2次 搬迁,搬完两个原始buckets中的2个完整的 bucket
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
整个 map 的搬迁操作,基本理顺了,其中比较让人困惑的是 h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2)
主要用来处理特殊的 ‘NaN’ 做 key,此时 map是没法取出 ‘NaN’ 的 value,因为其每次 hash值都不同。但是用 for 进行遍历时候,能够遍历出。
map 还有 delete操作和 遍历,delete 不做说明。对于遍历,golang 的map遍历是无序的,因为扩容后数据会重新迁移 bucket,顺序就是会改变(即便不是这样,golang 的map遍历也是随机从buckets中取一个开始,也是无需的),并且,在遍历时如果一个 map 尚处在扩容未搬迁完的 growing中,遍历如何操作呢?如果能够将上文的源码理解,再去理解map的遍历,基本手到擒来。
整个阅读过程,一定要先理解map结构,hmap,bmap,曹大的图看懂了,理解代码就容易。