嘿,小伙伴们!我是 k 哥。今天,咱们来聊聊 Map 。
在 Go 语言这个神奇的世界里,Map 这个有点神秘的数据结构一直都是开发者们特别关注的。
你是不是在用 Map 的时候,对它里面咋工作的感到好奇?是不是碰到复杂操作的时候,特别想弄明白它背后的原理?别着急,今天这篇文章就带你走进 Go 语言 Map 那个神秘的世界,帮你一层一层揭开它的面纱!
从底层的原理,到最佳实践,再到高频面试题的分析,这篇文章会从各个方面满足你的求知心。不管你是刚学的新手,还是经验丰富的老手,相信都能从这里得到宝贵的知识,受到启发。准备好跟我一起开始这场精彩的探索旅程了不?
1 原理
1.1 数据结构
Map 所涉及的核心数据结构包括两个,即 hmap 和 bmap :
-
每当创建一个 map 对象时,在底层都会创建一个 hmap 结构,以用于存储 map 的长度、hash 种子、状态等基础信息。
-
hmap 指针类型的成员变量 buckets ,指向 bmap 数组。bmap 用于存储键值对。对于相同的键,每次进行 hash 操作后都会定位到 buckets 相同的索引位置进行访问。
-
每个 bmap 能够存储 8 个键值对,并且,每个 bmap 设有一个指针,当某个 bmap 存满时,就会申请新的 bmap 进行存储,并与前一个 bmap 构成链表。(通过链地址法解决冲突)
1.1.1 hmap
const (// Maximum average load of a bucket that triggers growth is 6.5.// Represent as loadFactorNum/loadFactorDen, to allow integer math.loadFactorNum = 13loadFactorDen = 2)// 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 uint8B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0 uint32 // hash seedbuckets 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 growingnevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)extra *mapextra // optional fields
}// mapextra holds fields that are not present on all maps.
type mapextra struct {// 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.overflow *[]*bmap // 溢出桶数组指针oldoverflow *[]*bmap // 迁移过程中,旧溢出桶数组指针// nextOverflow holds a pointer to a free overflow bucket.nextOverflow *bmap // 还未使用的,提前分配的溢出桶链表
}
每创建一个 map 对象,在 Go 语言底层都会创建 hmap 结构,其成员的含义如下:
-
count :表示 map 中的数据个数,与 len() 函数相对应。
-
flags :属于标记字段,用于标记是否正在进行读写操作,以便实现并发读写的检测。
-
B :代表桶的数量,hash 桶 buckets 的数量为 2^B 个。
-
noverflow :是溢出桶数量的近似值。
-
hash0 :为 hash 种子,用于计算 key 的 hash 值。
-
buckets :是指向由 2^B 个桶所组成的数组的指针。
-
oldbuckets :指向扩容前的旧 buckets 数组,仅在 map 扩容时发挥作用。
-
nevacuate :作为计数器,用于标示扩容后搬迁的进度,服务于渐进式迁移。
-
extra :用于保存溢出桶和未使用溢出桶切片的首地址。
1.1.2 bmap
const (// Maximum number of key/elem pairs a bucket can hold.bucketCntBits = 3bucketCnt = 1 << bucketCntBits
)// 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.// Followed by an overflow pointer.
}
bmap 主要用于存储 key-value 对,每个 bmap 能够存储 8 个 key-value 对。bmap 包含 4 个成员变量(尽管在源码中只有一个成员变量 tophash,但实际上在申请内存时,Go 语言会依据 key、value 的具体类型,额外为另外 3 个成员变量分配内存):
-
tophash 数组,用于存储每个 key hash 之后的高位 hash 值。
-
key 数组,用于存储 key。
-
value 数组,用于存储 value。
-
overflow 溢出指针,指向了下一个 bmap 的地址。
bmap 有个溢出桶指针能指向溢出桶,那 extra 里为啥还得用 *[]*bmap 结构来存所有的溢出桶呢?
这主要是因为 gc 的原因。在早前的 Go 语言版本里,gc 会把 buckets 里的所有对象都扫一遍。要是 map 存的 key-value 对特别多,gc 能花上几百毫秒到好几秒,这就会让一些用 Go 语言开发的服务,接口超时超得厉害。
为了处理这个情况,Go 语言官方改了 map 的实现。要是 map 满足下面这两个条件,那 bmap 里除了 overflow ,就没别的指针了,而且会被 gc 标记成不用扫描:
-
key 和 value 不是指针类型,里面也不含指针(int 类型行,string 类型不行,因为 string 底层的数据结构里有指针)。
-
key 和 value 的大小得小于 128 字节。
但是 bmap.overflow 是指针类型,如果 gc 不扫 buckets ,溢出桶就可能被 gc 错误地回收掉。为了不让这种情况发生,就在 extra 里用 *[]*bmap 来存溢出桶,这样 gc 就能通过 []*bmap 扫到溢出桶(不用扫桶里面的东西),也就不会把溢出桶错误回收了。
1.2 插入或更新
1.2.1 2种异常情况处理
// Like mapaccess, but allocates a slot for the key if it is not present in the map.
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {// 为nil则panicif h == nil {panic(plainError("assignment to entry in nil map"))}// 并发读写会抛异常,且不能被defer捕获if h.flags&hashWriting != 0 {throw("concurrent map writes")}// 计算key对应的hash值hash := t.hasher(key, uintptr(h.hash0))// 设置正在写标记h.flags ^= hashWriting// 初始化,但是桶为空的,会自动创建桶数组,读写不会panicif h.buckets == nil {