具体源码在 runtime/map.go下面,有1300+行代码
golang 的map底层使用哈希表实现的,源码中对应是一个结构体,具体如下
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/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) map中元素的个数
flags uint8 //标识当前map是在读或者写的状态,每次操作会更新这个值
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) 桶的个数相关参数,2^B 是桶的个数
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details 溢出桶的个数
hash0 uint32 // hash seed hash的种子,用方法生成的随机值,每个map都不一样,所以同一个key在不同map位置也不一样
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 记录溢出相关信息
}
复制代码
每个map里面有若干桶,hmap里面只记录了桶的地址,找到具体桶内元素需要知道桶的结构,桶在源码中用bmap结构体表示,具体如下
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8 # bucketCnt为8 存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
复制代码
根据哈希函数将key生成一个哈希值,其中低位哈希用来判断桶位置,高位哈希用来确定在桶中哪个cell。低位哈希就是哈希值的低B位,hmap结构体中的B,比如B为5,2^5=32,即该map有32个桶,只需要取哈希值的低5位就可以确定当前key-value落在哪个桶(bucket)中;高位哈希即tophash,是指哈希值的高8bits,根据tophash来确定key在桶中的位置。每个桶可以存储8对key-value,存储结构不是key/value/key/value...,而是key/key..value/value,这样可以避免字节对齐时的padding,节省内存空间。
当不同的key根据哈希得到的tophash和低位hash都一样,发生哈希碰撞,这个时候就体现overflow pointer字段的作用了。桶溢出时,就需要把key-value对存储在overflow bucket(溢出桶),overflow pointer就是指向overflow bucket的指针。如果overflow bucket也溢出了呢?那就再给overflow bucket新建一个overflow bucket,用指针串起来就形成了链式结构
初始化map
// 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 {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()//赋随机值
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B//个数赋值
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
// If hint is large zeroing this memory could take a while.
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
}
复制代码
查找map中的元素
// 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)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {//若干目前标记是写,报panic不能并发读写
throw("concurrent map read and map write")
}
hash := t.hasher(key, uintptr(h.hash0))//计算key的哈希值
m := bucketMask(h.B)//计算桶链首地址
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
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
}
}
top := tophash(hash)// 计算tophash值
bucketloop:
for ; b != nil; b = b.overflow(t) {//遍历每一个桶
for i := uintptr(0); i < bucketCnt; i++ {//根据高8位找在桶内的元素
if b.tophash[i] != top {//tophash有8个元素,存储的key高8位信息
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if t.key.equal(key, k) {//最后的判断key相等
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e//找到直接返回
}
}
}
return unsafe.Pointer(&zeroVal[0])//没找到,返回对应类型的零值
}
复制代码
map的查找有两种用法,底层方法都是用的上面的mapaccess1,在mapaccess1返回参数的基础上在加一些判断,返回参数是一个,如下
m := make(map[string]int)
print(m["1"]) -- 结果为: 0 (返回int对应的默认值)
复制代码
对应的源码,很简单,直接调用mapaccess1然后返回
func mapaccess1_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) unsafe.Pointer {
e := mapaccess1(t, h, key)
if e == unsafe.Pointer(&zeroVal[0]) {
return zero
}
return e
}
复制代码
还有一种返回参数是两个,第二个参数标识map是否存在这个key,不存在,第一个参数就是默认零值
m := make(map[string]int)
val, ok := m["1"]
print(val, ok) -- 结果为:0, false
复制代码
对应的源码,相比第一种就是判断如果是默认零值就多返回一个参数false,否则返回true
func mapaccess2_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) (unsafe.Pointer, bool) {
e := mapaccess1(t, h, key)
if e == unsafe.Pointer(&zeroVal[0]) {
return zero, false
}
return e, true
}
复制代码
上面两种用法的源码mapaccess1_fat,和mapaccess2_fat是我猜测的,看源码感觉应该是这样的,没有验证过
map还有一种用发,使用for range遍历map元素,这里有一个考点是map乱序,1.map底层散列表在扩容场景下会重新散列,遍历顺序有变化 2.go设计者为了不让用户不依赖,每次遍历的桶的起点是不一样的
map 扩容策略
负载因子 = 键数量/bucket数量
哈希因子过小,说明空间利用率低
哈希因子过大,说明冲突严重,存取效率低
map的扩容机制从几个方面来讲:
1.扩容时机
map插入新key的时候,满足条件,就会触发扩容
2.扩容条件
1.超过负载
map元素个数 》 6.5 * 桶个数
平均一个桶有6.5个元素了,说明空间利用率很大
2.溢出桶太多
负载因子比较小的时候,溢出桶比较多,造成原因有 元素新增的时候新建了很多溢出桶,后来又删除了很多,就会造成 元素很少,负载因子小,但是溢出桶很多,元素分布比较稀疏,这个时候应该合并一些溢出桶
3.扩容机制
根据不同的扩容条件,有不同的扩容机制
1.超过负载
双倍扩容机制,创建新的buckets数组,大小是原来的两倍,把老的搬到新的桶里面
2.溢出桶比较多
等量扩容机制,桶的数量维持不变,把松散的键值重新排列一次,使得同一个桶里面的元素排列更紧密,减少溢出桶,节省空间
4.扩容方式
采用渐进式迁移,一次性全部迁移可能会造成比较大的时延,所以每次增删一个key的时候会去做搬迁
对于go1.6以上版本,如果出现【并发map读写】程序会直接以fatal error崩溃,即使同routine内有recover()也不能恢复
参考 www.bilibili.com/read/cv5588… blog.csdn.net/fengshenyun…