文章目录
Map访问源码
对于 v := map[key] 这种访问方式,在运行时其实调用的 runtime.mapaccess1 方法,源码如下:







go
// mapaccess1 returns a pointer to h[key]. Never returns nil, instead
// it will return a reference to the zero object for the elem type if
// the key is not in the map.
// NOTE: The returned pointer may keep the whole map live, so don't
// hold onto it for very long.
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
// hmap为空或者没有数据
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
// 返回零值
return unsafe.Pointer(&zeroVal[0])
}
// map正在被写操作,不允许读取,会有并发问题,发生fatal error
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 根据key和hash种子计算出hash值
hash := t.hasher(key, uintptr(h.hash0))
// 根据B计算mask
m := bucketMask(h.B)
// 根据hash的低B位计算出key在哪个bucket中,找到对应的bucket
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// oldbuckets 不为 nil,说明发生了扩容
if c := h.oldbuckets; c != nil {
// 不是等量扩容,说明是双倍扩容,使用旧桶的掩码查找
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
// 新 bucket 数量是老的 2 倍
m >>= 1
}
// 求出 key 在老的 map 中的 bucket 位置, 根据掩码和hash值找到这个key对应的桶
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
// 根据tophash[0]的状态为来判断当前桶是否还没有被迁移,还在旧桶中
// 如果 oldb 没有搬迁到新的 bucket
// 那就在老的 bucket 中寻找
if !evacuated(oldb) {
b = oldb
}
}
// 根据hash值计算出tophash
top := tophash(hash)
bucketloop: // 遍历当前桶和其指向的溢出桶( bucket 找完(还没找到),继续到 overflow bucket 里找)
for ; b != nil; b = b.overflow(t) {
// 遍历桶内的8个槽位
for i := uintptr(0); i < bucketCnt; i++ {
// 该槽位的tophash和当前key的tophash不相等,判断该槽位的tophash状态位
// 若状态位是"后继空状态",则说明后继没有数据了,可以提前退出,返回零值,否则就继续遍历下一个槽位
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 该槽位的tophash和当前key的tophash相等
// tophash 匹配,定位到 key 的位置
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
// key 是指针, 解引用
k = *((*unsafe.Pointer)(k))
}
// 继续判断当前槽位的对应的key于当前key是否相同, 相同就根据指针偏移查找到对应的value返回,不相同就继续遍历下一个槽位
// 如果 key 相等
if t.key.equal(key, k) {
// 定位到 value 的位置
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
// value 解引用
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
// overflow bucket 也找完了,说明没有目标 key
// 返回零值
return unsafe.Pointer(&zeroVal[0])
}
函数返回 h[key] 的指针,如果 h 中没有此 key,那就会返回一个 key 相应类型的零值,不会返回 nil。
这里,说一下定位 key 和 value 的方法以及整个循环的写法。
go
// key 定位公式
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
// value 定位公式
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
b 是 bmap 的地址,这里 bmap 还是源码里定义的结构体,只包含一个 tophash 数组,经编译器扩充之后的结构体才包含 key,value,overflow 这些字段。dataOffset 是 key 相对于 bmap 起始地址的偏移:
go
dataOffset = unsafe.Offsetof(struct {
b bmap
v int64
}{}.v)
因此 bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 个 key 的地址就要在此基础上跨过 i 个 key 的大小;而我们又知道,value 的地址是在所有 key 之后,因此第 i 个 value 的地址还需要加上所有 key 的偏移。 理解了这些,上面 key 和 value 的定位公式就很好理解了。
再说整个大循环的写法,最外层是一个无限循环,通过
go
b = b.overflow(t)
遍历所有的 bucket,这相当于是一个 bucket 链表。
当定位到一个具体的 bucket 时,里层循环就是遍历这个 bucket 里所有的 cell,或者说所有的槽位,也就是 bucketCnt=8 个槽位。整个循环过程:

再说一下 minTopHash,当一个 cell 的 tophash 值小于 minTopHash 时,标志这个 cell 的迁移状态。因为这个状态值是放在 tophash 数组里,为了和正常的哈希值区分开,会给 key 计算出来的哈希值一个增量:minTopHash。这样就能区分正常的 top hash 值和表示状态的哈希值。
下面的这几种状态就表征了 bucket 的情况:
go
// 空的 cell,也是初始时 bucket 的状态
empty = 0
// 空的 cell,表示 cell 已经被迁移到新的 bucket
evacuatedEmpty = 1
// key,value 已经搬迁完毕,但是 key 都在新 bucket 前半部分,
// 后面扩容部分会再讲到。
evacuatedX = 2
// 同上,key 在后半部分
evacuatedY = 3
// tophash 的最小正常值
minTopHash = 4
源码里判断这个 bucket 是否已经搬迁完毕,用到的函数:
go
func evacuated(b *bmap) bool {
h := b.tophash[0]
return h > empty && h < minTopHash
}
只取了 tophash 数组的第一个值,判断它是否在 0-4 之间。对比上面的常量,当 top hash 是 evacuatedEmpty、evacuatedX、evacuatedY 这三个值之一,说明此 bucket 中的 key 全部被搬迁到了新 bucket。
Map赋值源码
map的赋值操作在运行时,其实是调用了 runtime.mapassign 函数,源码如下:

mapassign 有一个系列的函数,根据 key 类型的不同,编译器会将其优化为相应的"快速函数"。


我们只用研究最一般的赋值函数 mapassign。
整体来看,流程非常得简单:对 key 计算 hash 值,根据 hash 值按照之前的流程,找到要赋值的位置(可能是插入新 key,也可能是更新老 key),对相应位置进行赋值。
源码大体和之前讲的类似,核心还是一个双层循环,外层遍历 bucket 和它的 overflow bucket,内层遍历整个 bucket 的各个 cell。
我这里会针对这个过程提几点重要的。
函数首先会检查 map 的标志位 flags。如果 flags 的写标志位此时被置 1 了,说明有其他协程在执行"写"操作,进而导致程序 fatal error。这也说明了 map 对协程是不安全的。
我们知道扩容是渐进式的,如果 map 处在扩容的过程中,那么当 key 定位到了某个 bucket 后 ,需要确保这个 bucket 对应的老 bucket 完成了迁移过程。即老 bucket 里的 key 都要迁移到新的 bucket 中来(分裂到 2 个新 bucket),才能在新的 bucket 中进行插入或者更新的操作。
make(map[K]V) 返回的是空的、非 nil 的 map(即 hmap 已经分配出来了)。
但在很多版本/实现里,如果初始容量很小(典型就是不传 hint),runtime 会采用懒初始化:h.buckets 可能先保持 nil,等到第一次写入(m[k]=v)时才真正分配 buckets。
上面说的操作是在函数靠前的位置进行的,只有进行完了这个搬迁操作后,我们才能放心地在新 bucket 里定位 key 要安置的地址,再进行之后的操作。
现在到了定位 key 应该放置的位置了,所谓找准自己的位置很重要。准备两个指针,一个(inserti)指向 key 的 hash 值在 tophash 数组所处的位置,另一个(insertk)指向 cell 的位置(也就是 key 最终放置的地址),当然,对应 value 的位置就很容易定位出来了。这三者实际上都是关联的,在 tophash 数组中的索引位置决定了 key 在整个 bucket 中的位置(共 8 个 key),而 value 的位置需要"跨过" 8 个 key 的长度。
在循环的过程中,inserti 和 insertk 分别指向第一个找到的空闲的 cell。如果之后在 map 没有找到 key 的存在,也就是说原来 map 中没有此 key,这意味着插入新 key。那最终 key 的安置地址就是第一次发现的"空位"(tophash 是 empty)。
如果这个 bucket 的 8 个 key 都已经放置满了,那在跳出循环后,发现 inserti 和 insertk 都是空,这时候需要在 bucket 后面挂上 overflow bucket。当然,也有可能是在 overflow bucket 后面再挂上一个 overflow bucket。这就说明,太多 key hash 到了此 bucket。
在正式安置 key 之前,还要检查 map 的状态,看它是否需要进行扩容。如果满足扩容的条件,就主动触发一次扩容操作。
这之后,整个之前的查找定位 key 的过程,还得再重新走一次。因为扩容之后,key 的分布都发生了变化。
最后,会更新 map 相关的值,如果是插入新 key,map 的元素数量字段 count 值会加 1;在函数之初设置的 hashWriting 写标志出会清零。
另外,有一个重要的点要说一下。前面说的找到 key 的位置,进行赋值操作,实际上并不准确。我们看 mapassign 函数的原型就知道,函数并没有传入 value 值,所以赋值操作是什么时候执行的呢?
go
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
mapassign 函数返回的指针就是指向的 key 所对应的 value 值位置,有了地址,就很好操作赋值了
go
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil { // map为空,不可写,直接返回
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := getcallerpc()
pc := abi.FuncPCABIInternal(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.key.size)
}
if asanenabled {
asanread(key, t.key.size)
}
if h.flags&hashWriting != 0 { // 并发问题,map正在被写,会有并发问题,发生fatal error
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0)) // 根据key和hash种子计算hash值
// 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的状态置为写
if h.buckets == nil { // 如果正常的桶数组为空,初始化桶数组
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
bucket := hash & bucketMask(h.B) // 根据hash值找到桶的位置
if h.growing() { // map正在扩容,将自己要使用的桶的数量迁移到新桶
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) // 跟访问时一样,这一步是获得目标桶指针,如果发生了扩容,则这个桶指针是新桶指针
top := tophash(hash) // 计算tophash
var inserti *uint8 // 待赋值的tophash指针
var insertk unsafe.Pointer // 待赋值的key指针
var elem unsafe.Pointer // 待赋值的value指针
bucketloop: // 循环遍历map中是否已经存在这个key
for {
for i := uintptr(0); i < bucketCnt; i++ { // 遍历一个桶中的8个槽位
if b.tophash[i] != top { // 槽位tophash与目标tophash不相等
// 判断该槽位的tophash是否为空,如果该槽位的tophash为空说明该槽位可能就是我们要找的插入目标键值对的位置
if isEmpty(b.tophash[i]) && inserti == nil {
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))
}
// 如果该槽位的态位为"后继空状态",说明key之前没有被插入过,这个位置就是真实的插入位置,找到了,退出循环
if b.tophash[i] == emptyRest {
break bucketloop
}
// 找的槽位不满足,遍历下一个槽位
continue
}
// 槽位的tophash与目标tophash相同,接着获取该槽位的key进行对比
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// tophash相同,但是key不同,继续遍历下一个槽位
if !t.key.equal(key, k) {
continue
}
// already have a mapping for key. Update it.
// 找到key了,更新这个槽位中的value为新的value
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done // 跳到结尾
}
// 遍历下一个溢出桶
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 走到这里,说明map中没有待赋值的key,需要新增键值对key/value
// 判断是否满足扩容条件
// 如果满足,先做好扩容准备,返回again再检查一次
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
}
// 到这一步,map中既没有找到key,根据这个key找到的桶及其这个桶的溢出桶中没有空的槽位了,要申请一个新的溢出桶
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,insertk,elem全部指向新的溢出桶的第一个槽
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// store new key/elem at insert position
// 在插入的位置存储key/value键值对
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++ // map元素数量+1
done: // 收尾工作
if h.flags&hashWriting == 0 { // 再次判断map是否正在被写入,这里应该是正在被写入,如果得到非,说明状态被改了,发生了并发写,报fatal error,相当于乐观锁
throw("concurrent map writes")
}
h.flags &^= hashWriting // 清除map的写状态
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}

只是判空,设置新kv的候选位置,如果满足就跳到下面进行插入新k-v,但如果存在旧k就会进行更新赋值,根据这些条件进行操作的

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!