Golang Map源码分析(二、基础操作之初始化、读取、写入、删除)
注意当前go版本代码为1.23
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 type hmap struct { count int flags uint8 B uint8 noverflow uint16 hash0 uint32 buckets unsafe.Pointer oldbuckets unsafe.Pointer nevacuate uintptr extra *mapextra }
上一篇文章我们介绍了map的结构与实现,接下来我们看看map的基础操作是如何实现的吧
基础操作 初始化 从语法层面上来说,创建 map 很简单:
1 2 3 4 5 6 7 8 9 10 11 12 mp := make (map [string ]int ) mp := make (map [string ]int , 8 ) mp := map [string ]int { "1" : 2 , "3" : 4 , "5" : 6 , }var mp map [string ]int
通过汇编语言可以看到,实际上底层调用的是 makemap
函数,主要做的工作就是初始化 hmap
结构体的各种字段,例如计算 B 的大小,设置哈希种子 hash0 等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 func makemap (t *maptype, hint int , h *hmap) *hmap { mem, overflow := math.MulUintptr(uintptr (hint), t.Bucket.Size_) if overflow || mem > maxAlloc { hint = 0 } if h == nil { h = new (hmap) } h.hash0 = uint32 (rand()) B := uint8 (0 ) for overLoadFactor(hint, B) { B++ } h.B = B 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 }func makeBucketArray (t *maptype, b uint8 , dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) { base := bucketShift(b) nbuckets := base if b >= 4 { nbuckets += bucketShift(b - 4 ) sz := t.Bucket.Size_ * nbuckets up := roundupsize(sz, !t.Bucket.Pointers()) if up != sz { nbuckets = up / t.Bucket.Size_ } } if dirtyalloc == nil { buckets = newarray(t.Bucket, int (nbuckets)) } else { buckets = dirtyalloc size := t.Bucket.Size_ * nbuckets if t.Bucket.Pointers() { memclrHasPointers(buckets, size) } else { memclrNoHeapPointers(buckets, size) } } if base != nbuckets { 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 }
hint
和 b
(在代码中为 B
)的意义如下:
hint
(int): hint
参数表示 map 预期的元素个数。它作为创建 map 的初始容量的参考。makemap
函数会根据 hint
的值来确定初始桶的数量和大小,以尽量减少后续的扩容操作,提高性能。 需要注意的是,hint
只是一个建议值,Go 运行时并不保证最终创建的 map 的容量与 hint
完全一致。如果 hint
的值过大或者计算过程中发生溢出,hint
会被重置为 0。
B
(uint8): B
决定了 map 的桶数组的大小。桶数组的大小为 2 的 B
次方。B
值越大,桶数组越大,可以容纳的元素越多,但同时也会占用更多的内存。B
的值是通过 overLoadFactor
函数计算得出的,该函数会根据 hint
值和当前的 B
值判断是否需要增加 B
的值。makemap
函数会不断增加 B
的值,直到找到一个合适的 B
值,使得 map 能够容纳 hint
个元素,并且负载因子不会过高。
makemap
函数的目标是创建一个合适的哈希表,以存储用户指定数量的元素。为了实现这个目标,它需要确定两个关键参数:
桶的数量: 桶的数量越多,哈希冲突的概率就越小,但同时也会占用更多的内存。
桶的大小: 每个桶可以存储多个键值对。桶越大,可以存储的键值对越多,但查找效率会降低。
hint
参数用于指导 makemap
函数确定桶的数量。makemap
函数会根据 hint
的值计算出一个合适的 B
值,然后使用 2 的 B
次方作为桶的数量。
overLoadFactor
函数用于判断当前的 B
值是否足够大。如果 hint
除以 2 的 B
次方(即桶的数量)的结果大于一个预定的负载因子,则说明当前的 B
值不够大,需要继续增加。
通过这种方式,makemap
函数可以根据 hint
的值动态地调整桶的数量,以确保 map 具有合适的容量和性能。
总而言之,hint
提供了一个创建 map 的容量建议,而 B
是 makemap
函数根据 hint
计算得出的一个内部参数,它决定了 map 底层桶数组的大小,从而影响 map 的性能和内存占用。
读取 上一篇文章《Golang Map源码分析(一、定义与实现原理)》 已经提到过key定位过程,其实就是get操作的主要逻辑,总而言之, map get
的过程就是:前置检查 -> 计算哈希值 -> 定位桶 -> 遍历桶内条目 -> 比较 tophash 和 key -> 返回 value 指针 (或零值指针)。
主要源码如下 runtime.mapaccess1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 func mapaccess1 (t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { if h == nil || h.count == 0 { if err := mapKeyError(t, key); err != nil { panic (err) } return unsafe.Pointer(&zeroVal[0 ]) } if h.flags&hashWriting != 0 { fatal("concurrent map read and map write" ) } hash := t.Hasher(key, uintptr (h.hash0)) m := bucketMask(h.B) b := (*bmap)(add(h.buckets, (hash&m)*uintptr (t.BucketSize))) if c := h.oldbuckets; c != nil { if !h.sameSizeGrow() { m >>= 1 } oldb := (*bmap)(add(c, (hash&m)*uintptr (t.BucketSize))) if !evacuated(oldb) { b = oldb } } top := tophash(hash) bucketloop: for ; b != nil ; b = b.overflow(t) { for i := uintptr (0 ); i < abi.OldMapBucketCount; i++ { if b.tophash[i] != top { 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) { e := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr (t.KeySize)+i*uintptr (t.ValueSize)) if t.IndirectElem() { e = *((*unsafe.Pointer)(e)) } return e } } } return unsafe.Pointer(&zeroVal[0 ]) }
这里再补充一下get的两种实现方式
1 2 v := m[key] v, ok := m[key]
赋值语句左侧接受参数的个数会决定使用的运行时方法:
另外,根据 key 的不同类型,编译器还会将查找、插入、删除的函数用更具体的函数替换,以优化效率:
key 类型
查找
uint32
mapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer
uint32
mapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool)
uint64
mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer
uint64
mapaccess2_fast64(t *maptype, h *hmap, key uint64) (unsafe.Pointer, bool)
string
mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer
string
mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool)
这些函数的参数类型直接是具体的 uint32、unt64、string,在函数内部由于提前知晓了 key 的类型,所以内存布局是很清楚的,因此能节省很多操作,提高效率。
删除 源码位置:runtime.mapdelete
。
哈希表的删除逻辑与写入逻辑主要流程很相似
它首先会检查 h.flags 标志,如果发现写标位是 1,直接 panic,因为这表明有其他协程同时在进行写操作。
计算 key 的哈希,找到落入的 bucket。如果在删除期间遇到了哈希表的扩容,就会分流桶中的元素进行扩容,分流结束之后会找到桶中的目标元素完成键值对的删除工作。
找到对应位置后,对 key 或者 value 进行清除操作
最后,将 count 值减 1,将对应位置的 tophash 值置成 Empty
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 func mapdelete (t *maptype, h *hmap, key unsafe.Pointer) { if h == nil || h.count == 0 { if err := mapKeyError(t, key); err != nil { panic (err) } return } if h.flags&hashWriting != 0 { fatal("concurrent map writes" ) } hash := t.Hasher(key, uintptr (h.hash0)) h.flags ^= hashWriting bucket := hash & bucketMask(h.B) if h.growing() { growWork(t, h, bucket) } b := (*bmap)(add(h.buckets, bucket*uintptr (t.BucketSize))) bOrig := b top := tophash(hash) search: for ; b != nil ; b = b.overflow(t) { for i := uintptr (0 ); i < abi.OldMapBucketCount; i++ { if b.tophash[i] != top { if b.tophash[i] == emptyRest { break search } continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr (t.KeySize)) k2 := k if t.IndirectKey() { k2 = *((*unsafe.Pointer)(k2)) } if !t.Key.Equal(key, k2) { continue } if t.IndirectKey() { *(*unsafe.Pointer)(k) = nil } else if t.Key.Pointers() { memclrHasPointers(k, t.Key.Size_) } e := add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr (t.KeySize)+i*uintptr (t.ValueSize)) if t.IndirectElem() { *(*unsafe.Pointer)(e) = nil } else if t.Elem.Pointers() { memclrHasPointers(e, t.Elem.Size_) } else { memclrNoHeapPointers(e, t.Elem.Size_) } b.tophash[i] = emptyOne if i == abi.OldMapBucketCount-1 { if b.overflow(t) != nil && b.overflow(t).tophash[0 ] != emptyRest { goto notLast } } else { if b.tophash[i+1 ] != emptyRest { goto notLast } } for { b.tophash[i] = emptyRest if i == 0 { if b == bOrig { break } c := b for b = bOrig; b.overflow(t) != c; b = b.overflow(t) { } i = abi.OldMapBucketCount - 1 } else { i-- } if b.tophash[i] != emptyOne { break } } notLast: h.count-- if h.count == 0 { h.hash0 = uint32 (rand()) } break search } } if h.flags&hashWriting == 0 { fatal("concurrent map writes" ) } h.flags &^= hashWriting }
问题 可以对 map 的元素取地址吗 无法对 map 的 key 或 value 进行取址。以下代码不能通过编译:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { m := make (map [string ]int ) fmt.Println(&m["test" ]) }
编译报错:
1 ./main.go:8:14: cannot take the address of m["qcrao"]
如果通过其他 hack 的方式,例如 unsafe.Pointer 等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。
map 是线程安全的吗 map 不是线程安全的。
在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。赋值和删除函数在检测完写标志是复位之后,先将写标志位置位,才会进行之后的操作。
1 2 3 4 if h.flags&hashWriting == 0 { throw("concurrent map writes" ) }
参考链接 1.Go 程序员面试笔试宝典
2.《Go学习笔记》
3.Go 语言设计与实现