嘿,小伙伴们!我是 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 = 13
loadFactorDen = 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 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
}
// 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 = 3
bucketCnt = 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则panic
if 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
// 初始化,但是桶为空的,会自动创建桶数组,读写不会panic
if h.buckets == nil {