一、什么是Map以及Go语言的Map

1.1 Map的定义

Map是一种由key-value键值对组成的集合,且key只出现一次。也被称为字典,关系数组等。
Map主要有两种数据结构来实现:哈希查找表(Hash table)、搜索树(Search tree)

1.2 哈希查找表

哈希查找表是通过哈希函数将key分配到不同的bucket,访问时间复杂度最好为O(1),最差为O(n)。

哈希查找表会存在哈希碰撞问题,不同的key通过哈希函数计算出来的哈希值一样,使得这些key分配到了同一个bucket(简单理解,bucket就是不同的底层数组的索引)。一般处理哈希碰撞问题有两种方法:链表法开放地址法
链表法:将出现哈希碰撞而分配到同一个bucket的key,通过链表的方式存储。
开放地址法:出现哈希碰撞时,通过一定的规律,在该bucket底层数组的后面选择空闲位置,用来放置碰撞的那些key。

1.3 搜索树

搜索树一般使用的是自平衡搜索树:AVL树、红黑树。

自平衡搜索树在遍历时会返回从小到大的有序的key,而哈希查找表在遍历时则是无序的。不同的语言在实现Map时可能会采用不同的数据结构去实现,Go语言在实现Map时采用的是哈希查找表的方式。

Map实现了对数据的增删改查功能,且效率相当高效。
将Map和切片Slice对比来说:

  • 切片Slice是对一块连续内存上的数据的统一管理。
  • Map则是对不连续内存上数据的统一管理,不同的内存在Map结构中存在关联性。

    二、Map的创建

    创建Map通过var声明的方式或者make()函数的方式创建。 ```go // nil map 需要使用make()初始化后才能添加值 var testMap map[string]string

// 初始化并赋值 map var testMap1 = map[string]string{ “a”: “张三”, “b”: “李四”, “c”: “王五”, }

// 初始化并分配内存 testMap2 := make(map[string]string, 10)

  1. 那么,Map到底是如何创建的呢?以下,我们简单的了解一下Map的创建过程。<br />Map结构在Go语言中定义的结构体是:
  2. ```go
  3. // 源码路径:go/1.14.1/libexec/src/runtime/map.go:115
  4. // A header for a Go map.
  5. type hmap struct {
  6. count int // map 中元素的个数,len(map) 时直接返回该值
  7. flags uint8 // 标记位
  8. B uint8 // 计算buckets 数组的长度使用。buckets 的长度是2^B
  9. noverflow uint16 // 从bucket中溢出的数量。(overflow buckets 存储在extra中)
  10. hash0 uint32 // hash 因子
  11. buckets unsafe.Pointer // 指向 buckets(哈希桶) 数组的指针 大小是 2^B. 如果count==0,可能是nil.
  12. oldbuckets unsafe.Pointer // 发生扩容时,指向 旧buckets 数组的指针,是 新buckets 的一半,非nil时扩容
  13. nevacuate uintptr // 扩容进度,小于此地址的buckets表示迁移完毕
  14. extra *mapextra // 扩展字段,可选,在溢出(overflow)或者数据增长是使用
  15. }

在执行创建一个Map结构时,会调用到makemap()函数

  1. //源码路径:go/1.14.1/libexec/src/runtime/map.go:303
  2. // makemap() 用来执行 make(map[k]v, hint) 的逻辑。
  3. // 如果编译器已经确定 该map或者第一个bucket可以在堆栈上创建,
  4. // 那么 h和bucket 可能都是非nil,或者 h或bucket 可能有一个是非nil。
  5. // 如果 h != nil, 可以直接在h中创建该map。
  6. // 如果 h.buckets != nil, 则h.buckets 指向的bucket 可以作为第一个 bucket.
  7. func makemap(t *maptype, hint int, h *hmap) *hmap {
  8. mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
  9. if overflow || mem > maxAlloc {
  10. hint = 0
  11. }
  12. // 初始化 Hmap 结构体
  13. if h == nil {
  14. h = new(hmap)
  15. }
  16. h.hash0 = fastrand()
  17. // 计算hmap结构体中的B的值,通过make()创建时指定的int值
  18. B := uint8(0)
  19. for overLoadFactor(hint, B) {
  20. B++
  21. }
  22. h.B = B
  23. // 分配内存并初始化哈希表
  24. // 如果 B == 0,h.buckets将暂时不分配内存
  25. // 如果hint值太大,则初始化分配的内存可能时间长一些.
  26. if h.B != 0 {
  27. // 定义bmap结构体 一个bmap即为一个bucket
  28. var nextOverflow *bmap
  29. // 创建buckets和nextOverflow
  30. h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
  31. if nextOverflow != nil {
  32. // 指定extra的信息
  33. h.extra = new(mapextra)
  34. h.extra.nextOverflow = nextOverflow
  35. }
  36. }
  37. // 返回结构体指针
  38. return h
  39. }
  40. // ********************************************************
  41. // 源码路径: go/1.14.1/libexec/src/runtime/map.go:149
  42. // map 中存储具体元素k-v信息的 bucket 结构体.即hmap.buckets包含的单个bucket
  43. // 每个bucket存储8个键值对
  44. type bmap struct {
  45. // tophash包含每个key键哈希值的的顶部字节(高八位),简单来说就是记录该key键在bucket中的存放位置。
  46. // 如果tophash[0]<minTopHash 则表明tophash[0]处于迁移状态。
  47. // bucketCnt为8
  48. tophash [bucketCnt]uint8
  49. // 接着是8个key键和8个value元素。
  50. // 虽然k1,k2,k3...k8,v1,v2,v3...v8这种排列比k1,v1,k2,v2...k8,v8结构更加复杂,
  51. // 但是考虑到内存对齐,前一种方式更加合适。
  52. // 如 map[int64]int8,其中key是int64占8个字节,value是int8占1个字节,
  53. // kv长度不同,如果按照k,v交替的方式存放,则内存对齐的缘故,v也需要占用8个字节,
  54. // 如果k,k,v,v方式存放,则8个value则刚好占用一个key的字节空间。
  55. }
  56. // ********************************************************
  57. // 源码位置:go/1.14.1/libexec/src/runtime/map.go:132
  58. // mapextra 表示map中的extra扩展字段,
  59. type mapextra struct {
  60. // 如果key和value都不包含指针并且是内联的,那么我们就标记bucket不含指针,这样可以避免gc扫描
  61. // 但是bmap.overflow是一个指针。
  62. // 为了确保overflow buckets存活,我们会在hmap.extra.overflow和hmap.extra.oldoverflow中添加所有overflow buckets。
  63. // overflow和oldoverflow仅仅在key和value都不是指针时使用。
  64. // overflow包含hmap.buckets中的overflow buckets。
  65. // oldoverflow包含hmap.oldbuckets中的overflow buckets。
  66. // 间接允许将指向切片的指针存储在hiter中。
  67. overflow *[]*bmap
  68. oldoverflow *[]*bmap
  69. // nextOverflow拥有一个指向空闲overflow bucket的指针。(预分配的)
  70. nextOverflow *bmap
  71. }

通过对以上Map结构体、创建过程的源码的分析,我们可以总结出Map的创建过程:

  1. make(map[k]v, hint)中hint的值进行边界检查;
  2. 初始化hmap
  3. 计算hash因子;
  4. 通过hint计算B的最小值;
  5. 分配内存并初始化哈希表,如果B是0则之后再分配内存,否则立刻分配;
  6. 返回初始化完成的hmap的指针。

创建流程图为:
Map-第 2 页.jpg
创建完成的Map的结构为,其中bmap0为一个bucket,tophash用于快速判断key是否在该bucket中:
Map.jpg

三、Map的查询

Map查询某个key的value值使用两种方式:

  1. value := map[key],返回value值,如果key不存在,则返回value类型的零值;
  2. value,ok := map[key] ,返回value值和ok值,其中ok是一个布尔值,用来标记key是否存在;value同上返回零值。

Map键值的查询在源码中使用的是mapaccess系列函数。
其中具有代表性的是以下两个:

  • func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {...}

mapaccess1()对应第1种取值方式,返回value的指针地址,若不存在则返回零值指针。

  • func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {...}

mapaccess2()对应第2种取值方式,返回value的指针地址和key是否存在的布尔值。

mapaccess1()函数为例,分析源码中查询键值信息的步骤:
注:mapaccess2()mapaccess1()逻辑几乎一样,只是多返回了一个bool值。

  1. // 源码路径:go/1.14.1/libexec/src/runtime/map.go:394
  2. // mapaccess1 返回h[key]的指针地址,永不返回nil,如果key不存在则返回value类型的零值
  3. func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  4. if raceenabled && h != nil {
  5. callerpc := getcallerpc()
  6. pc := funcPC(mapaccess1)
  7. racereadpc(unsafe.Pointer(h), callerpc, pc)
  8. raceReadObjectPC(t.key, key, callerpc, pc)
  9. }
  10. if msanenabled && h != nil {
  11. msanread(key, t.key.size)
  12. }
  13. // 判断h是否是nil,或者h.count是否是0,若是则返回零值
  14. if h == nil || h.count == 0 {
  15. if t.hashMightPanic() {
  16. t.hasher(key, 0) // see issue 23734
  17. }
  18. return unsafe.Pointer(&zeroVal[0])
  19. }
  20. // 判断当前h是否并发读写,若是则抛出异常
  21. if h.flags&hashWriting != 0 {
  22. throw("concurrent map read and map write")
  23. }
  24. // 计算key的hash值
  25. hash := t.hasher(key, uintptr(h.hash0))
  26. // 计算key所在bucket的序号
  27. m := bucketMask(h.B)
  28. // 根据序号找到bucket地址
  29. b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
  30. // 如果h.oldbuckets有值,即正在发生扩容。
  31. if c := h.oldbuckets; c != nil {
  32. if !h.sameSizeGrow() {
  33. // There used to be half as many buckets; mask down one more power of two.
  34. m >>= 1
  35. }
  36. // 若扩容未迁移完毕,则到旧的buckets中查询
  37. oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
  38. if !evacuated(oldb) {
  39. b = oldb
  40. }
  41. }
  42. // 计算key hash的tophash
  43. top := tophash(hash)
  44. // 遍历查询key的位置
  45. bucketloop:
  46. for ; b != nil; b = b.overflow(t) {
  47. for i := uintptr(0); i < bucketCnt; i++ {
  48. if b.tophash[i] != top {
  49. // 如果本次没匹配到,而且其他cell又全是空,则直接break
  50. if b.tophash[i] == emptyRest {
  51. break bucketloop
  52. }
  53. continue
  54. }
  55. // 计算key的位置
  56. k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  57. if t.indirectkey() {
  58. k = *((*unsafe.Pointer)(k))
  59. }
  60. // 查询到key的位置,则返回value的指针地址
  61. if t.key.equal(key, k) {
  62. e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  63. if t.indirectelem() {
  64. e = *((*unsafe.Pointer)(e))
  65. }
  66. return e
  67. }
  68. }
  69. }
  70. // 没查找到,则返回value类型零值
  71. return unsafe.Pointer(&zeroVal[0])
  72. }

根据源码信息,可以得出value := map[key]的执行流程为:

  1. 判断h是否是nil,或者h.count是否是0,若是则返回零值;
  2. 判断当前h是否并发读写,若是则抛出异常;
  3. 计算key的hash值;
  4. 计算key所在bucket的地址;
  5. 如果h.oldbuckets有值,即正在发生扩容。若扩容未迁移完毕,则到旧的buckets中查询;
  6. 遍历查询key的位置,查询到key的位置,则返回value的指针地址,若没查到,则返回value类型零值。

图示为:
Map1-第 3 页.jpg
我们通过一张图能很好的理解,key是如何通过hash和tophash找到具体存放位置的:
image.png

四、Map的添加、更新

Map的增加和更新的使用方式比较简单,直接通过 map[key] = value 增加和更新键值对信息。

在Map中,对元素的添加和更新是通过mapassign()函数实现的:

  1. // 源码路径: go/1.14.1/libexec/src/runtime/map.go:571
  2. // 与mapaccess类似,但如果key不在map中,则为其分配一个位置
  3. func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  4. // h为nil,抛异常
  5. if h == nil {
  6. panic(plainError("assignment to entry in nil map"))
  7. }
  8. if raceenabled {
  9. callerpc := getcallerpc()
  10. pc := funcPC(mapassign)
  11. racewritepc(unsafe.Pointer(h), callerpc, pc)
  12. raceReadObjectPC(t.key, key, callerpc, pc)
  13. }
  14. if msanenabled {
  15. msanread(key, t.key.size)
  16. }
  17. // h正在并发读写,抛异常
  18. if h.flags&hashWriting != 0 {
  19. throw("concurrent map writes")
  20. }
  21. // 计算key 的hash
  22. hash := t.hasher(key, uintptr(h.hash0))
  23. // 计算hash值后,设置hashWriting。因为t.hasher可能抛异常,在这种情况下,我们实际上还没有写
  24. h.flags ^= hashWriting
  25. // 如果buckets为nil,则初始化
  26. if h.buckets == nil {
  27. h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
  28. }
  29. again:
  30. // 获取bucket的序号
  31. bucket := hash & bucketMask(h.B)
  32. // 若正在扩容,则继续迁移
  33. if h.growing() {
  34. growWork(t, h, bucket)
  35. }
  36. // 计算bucket中bmap的指针地址
  37. b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
  38. // 计算key hash的tophash
  39. top := tophash(hash)
  40. var inserti *uint8
  41. var insertk unsafe.Pointer
  42. var elem unsafe.Pointer
  43. // 遍历判断key的tophash和buckets中的是否一致
  44. bucketloop:
  45. for {
  46. for i := uintptr(0); i < bucketCnt; i++ {
  47. // 如果tophash不一致,则寻找空闲位置,并记录该位置,此时记录的是第一个空闲可插入数据的位置
  48. if b.tophash[i] != top {
  49. if isEmpty(b.tophash[i]) && inserti == nil {
  50. inserti = &b.tophash[i]
  51. insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  52. elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  53. }
  54. if b.tophash[i] == emptyRest {
  55. break bucketloop
  56. }
  57. continue
  58. }
  59. k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  60. if t.indirectkey() {
  61. k = *((*unsafe.Pointer)(k))
  62. }
  63. // 判断key与当前遍历到的k是否相等,不相等则跳过
  64. if !t.key.equal(key, k) {
  65. continue
  66. }
  67. // 如果key存在,则更新
  68. if t.needkeyupdate() {
  69. typedmemmove(t.key, k, key)
  70. }
  71. elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  72. // 结束,直接跳到done代码块
  73. goto done
  74. }
  75. // 遍历完毕则结束遍历
  76. ovf := b.overflow(t)
  77. if ovf == nil {
  78. break
  79. }
  80. // 当前遍历到的位置
  81. b = ovf
  82. }
  83. // 没找到则分配新的cell,即bucket的空位置
  84. // 如果并未扩容 且 达到了最大的负载系数 overLoadFactor 或 太多溢出桶 bucket overflow,则开始进行扩容
  85. if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
  86. hashGrow(t, h)
  87. goto again // Growing the table invalidates everything, so try again
  88. }
  89. // 此时,没找到空闲的位置,则需要新开辟一块进行键值对的存储
  90. if inserti == nil {
  91. // 当前所有的buckets都满了,需要分配一个新的
  92. newb := h.newoverflow(t, b)
  93. inserti = &newb.tophash[0]
  94. insertk = add(unsafe.Pointer(newb), dataOffset)
  95. elem = add(insertk, bucketCnt*uintptr(t.keysize))
  96. }
  97. // 放置k-v
  98. if t.indirectkey() {
  99. kmem := newobject(t.key)
  100. *(*unsafe.Pointer)(insertk) = kmem
  101. insertk = kmem
  102. }
  103. if t.indirectelem() {
  104. vmem := newobject(t.elem)
  105. *(*unsafe.Pointer)(elem) = vmem
  106. }
  107. typedmemmove(t.key, insertk, key)
  108. *inserti = top
  109. h.count++
  110. done:
  111. if h.flags&hashWriting == 0 {
  112. throw("concurrent map writes")
  113. }
  114. h.flags &^= hashWriting
  115. if t.indirectelem() {
  116. elem = *((*unsafe.Pointer)(elem))
  117. }
  118. // 返回value的指针地址
  119. return elem
  120. }

通过对源码的分析我们可以梳理出Map增加和修改元素的过程为:

  1. 判断h是否为nil;
  2. 判断h是否正在并发读写;
  3. 计算key 的hash 并设置hashWriting;
  4. 如果buckets为nil,则初始化;
  5. 获取key的bucket的序号;
  6. 若map正在扩容迁移,则继续还行迁移;
  7. 找到bucket中bmap的地址,并计算key的tophash;
  8. 进入遍历:遍历判断key的tophash和buckets中的是否一致;
  9. 遍历中:如果tophash不一致,则寻找空闲位置,并记录该位置,此时记录的是第一个空闲可插入数据的位置;
  10. 遍历中:如果key存在,则更新,结束遍历。然后返回value的地址;
  11. 遍历中:如果key没找到,则结束遍历,往下执行;
  12. 没找到key则分配新的cell,即bucket的空位置;
  13. 如果并未扩容 且 达到了最大的负载系数 overLoadFactor 或 太多溢出桶 bucket overflow,则开始进行扩容;
  14. 如果没找到空闲的位置,则新开辟一块内存进行键值对的存储,即overflow bucket;
  15. 返回value的地址。

整个添加和修改元素的逻辑是比较长的,含有,基本判断,逻辑计算,遍历对比,开辟新空间放置kv等逻辑。
最终的赋值并没有在mapassign()函数中进行,而是最后返回了value的指针。

五、Map的删除

删除Map键值对是通过 delete(map, key) 函数进行。其中map参数表示要删除键值对的Map,key参数表示Map的key键。

Map中的delete()操作是通过源码中的mapdelete()函数完成的。
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer){...}
具体执行逻辑为:

  1. 判断h是否为nil,或者h.count是否是0,若是则直接返回;
  2. 判断是否有其他协程在写操作,若有,则抛异常;
  3. 计算key的hash并找到key所在的bucket序号;
  4. 如果正在扩容迁移,则继续迁移;
  5. 获取key所在bmap地址,并计算key的tophash;
  6. 遍历找寻key的位置。找到后将该位置的k-v清零,将count-1,将bmap原tophash位置标记为Empty。

    六、总结

    以上我们了解了Go语言Map的结构,Map的创建,Map中元素的查询、添加、修改和删除的实现逻辑。
    Map是使用哈希查找表法实现的,并通过链表法解决哈希冲突。
    通过key的哈希值将不同的key分配到不同的bucket(哈希桶)中,每个bucket存储8个键值对(8个cell)。
    如果一个bucket不够用,元素占满了8个cell,则会分配一个新的overflow bucket(溢出桶),并通过bucket的overflow指针字段关联。
    查询、添加、修改、删除操作在底层源码中核心的逻辑在于通过key去获取具体的bucket信息。

最后,我们还需要知道以下关于Map的信息:

  • Map遍历通过for...range...的方式,并且Map的遍历是无序的;
  • Map的key非必要不要用float浮点数,以及NaN非数;
  • Map不是一个线程安全的结构,同时读写一个Map会报错。如有需要可以使用sync.Map包;
  • 创建Map的函数makemap()返回的是一个指针,创建Slice函数makeslice()函数的返回的是一个结构体。所以,当 Map 和 Slice 作为函数参数时,在函数参数内部对 Map 的操作会影响 Map 自身;而对 Slice 却不会(如对Slice进行append()之后,原Slice并不会更改)。

    主要原因为:一个是指针( hmap),一个是结构体( slice)。Go 语言中的函数传参都是值传递,在函数内部,参数会被 copy 到本地。hmap指针 copy 完之后,仍然指向同一个 map,因 此函数内部对 map 的操作会影响实参。而 slice 被 copy 后,会成为一个新的 slice,对它进行的操作不会影响到实参。

参考文档:
深度解密Go语言之map
深入Go的Map使用和实现原理
字典变量里存的是什么?
Go基础系列:map类型
Go语言基础之map