1.三万字带你认识 Go 底层 map 的实现
2.Go语言学习(2)--map的底层原理
3.Go实例讲解,并发编程-map并发读写的线程安全性问题
4.goland mapåºå±åç
5.map在golang的底层实现和源码分析
6.go map and slice 2021-10-08
三万字带你认识 Go 底层 map 的实现
map在Go语言中是一种基础数据结构,广泛应用于日常开发。其设计遵循“数组+链表”的通用思路,但Go语言在具体实现上有着独特的设计。本文将带你深入了解Go语言中map的云顶至尊源码底层实现,包括数据结构设计、性能优化策略以及关键操作的内部实现。
在Go语言的map中,数据存储在数组形式的桶(bucket)中,每个桶最多容纳8对键值对。哈希值的低位用于选择桶,而高位则用于在独立的桶中区分键。这种设计有助于高效地处理冲突和实现快速访问。
源码位于src/runtime/map.go,展示了map的内部结构和操作。在该文件中,定义了桶和map的内存模型,桶的内存结构示例如下。每个桶的前7-8位未被使用,用于存储键值对,避免了不必要的内存填充。在桶的末尾,还有一个overflow指针,用于连接超过桶容量的键值对,以构建额外的桶。
初始化map有两种方式,根据是否指定初始化大小和hint值,调用不同的函数进行分配。对于不指定大小或hint值小于8的情况,使用make_small函数直接在堆上分配。当hint值大于8时,调用makemap函数进行初始化。
插入操作的核心是找到目标键值对的内存地址,并通过该地址进行赋值。在实现中,没有直接将值写入内存,刷qq空间人气源码而是返回值在内存中的对应地址,以便后续进行赋值操作。同时,当桶达到容量上限时,会创建新的溢出桶来容纳多余的数据。
查询操作通过遍历桶来实现,找到对应的键值对。对于查询逻辑的优化,Go语言提供了不同的函数实现,如mapaccess1、mapaccess2和mapaccessK等,它们在不同场景下提供高效的关键字查找和值获取。
当map需要扩容时,Go语言会根据装载因子进行决策,以保持性能和内存使用之间的平衡。扩容操作涉及到数据搬移,通过hashGrow()和growWork()函数实现。增量扩容增加桶的数量,而等量扩容则通过重新排列元素提高桶的利用率。
删除操作在Go语言中同样高效,利用map的内部机制快速完成。迭代map时,可以使用特定的函数遍历键值对,实现对数据的访问和操作。
通过深入分析Go语言中map的实现,我们可以看到Go开发者在设计时的巧妙和全面考虑,不仅关注内存效率,还考虑到数据结构在不同情况下的复用和性能优化。这种设计思想不仅体现在map自身,也对后续的缓存库等开发产生了深远的影响。
综上所述,Go语言中map的底层实现展示了高效、灵活和强大的设计原则,为开发者提供了强大的工具,同时也启发了其他数据结构和库的设计。了解这些细节有助于我们更深入地掌握Go语言的七夕表白源码特性,并在实际开发中做出更优的选择。
Go语言学习(2)--map的底层原理
Golang的Map底层是通过HashTable实现的,创建map时实际返回的是runtime/map.go中hmap对象的指针。hmap中buckets指向的是bucket数组的指针,bucket数组大小由B决定,通常为2^B个。单个bucket结构体内部不直接定义keys、values和overflow,而是通过指针运算访问。
在查找、插入和删除过程中,通过哈希函数将键转换为哈希值,然后使用哈希值对bucket进行定位。查找时直接访问哈希表中对应的bucket,插入和删除操作涉及更新bucket中的键值对。
Map的扩容机制基于负载因子,负载因子用于衡量冲突情况,定义为bucket数量与键值对数量的比值。当负载因子大于6.5,或者overflow数量超过时,Map会触发扩容。扩容时,新bucket长度为原bucket长度的2倍,旧bucket数据搬迁到新bucket。为了减少一次性搬迁带来的延迟,Go采用逐步搬迁策略,每次访问map时触发搬迁,每次搬迁2个键值对。
扩容后,新bucket存储新插入的键值对,老bucket中的键值对逐步搬迁到新bucket。搬迁完成后,删除老bucket。搬迁过程中,老bucket中的键值对将位于新bucket的前部,新插入的c语言库函数源码键值对位于新bucket的后部。
等量扩容是重新组织bucket,提升bucket的使用率,而不是简单地增加容量。在某些极端场景下,如果键值对集中在少数bucket,可能导致overflow的bucket数量增多,但负载因子不高,无法执行增量搬迁。这时进行一次等量扩容,可以减少overflow的bucket数量,优化访问效率。
Go实例讲解,并发编程-map并发读写的线程安全性问题
先上实例代码,后面再来详细讲解。
/** * 并发编程,map的线程安全性问题,使用互斥锁的方式 */ package main import ( "sync" "time" "fmt" ) var data map[int]int = make(map[int]int) var wgMap sync.WaitGroup = sync.WaitGroup{ } var muMap sync.Mutex = sync.Mutex{ } func main() { // 并发启动的协程数量 max := wgMap.Add(max) time1 := time.Now().UnixNano() for i := 0; i < max; i++ { go modifySafe(i) } wgMap.Wait() time2 := time.Now().UnixNano() fmt.Printf("data len=%d, time=%d", len(data), (time2-time1)/) } // 线程安全的方法,增加了互斥锁 func modifySafe(i int) { muMap.Lock() data[i] = i muMap.Unlock() wgMap.Done() }
上面的代码中 var data map[int]int 是一个key和value都是int类型的map,启动的协程并发执行时,也只是非常简单的对 data[i]=i 这样的一个赋值操作。
主程序发起1w个并发,不断对map中不同的key进行赋值操作。
在不安全的情况下,我们直接就看到一个panic异常信息,程序是无法正常执行完成的,如下:
fatal error: concurrent map writes goroutine [running]: runtime.throw(0x4d6e, 0x) C:/Go/src/runtime/panic.go: +0x9c fp=0xcbf sp=0xcbf pc=0xac runtime.mapassign_fast(0x4ba4c0, 0xce, 0xc, 0x0) C:/Go/src/runtime/hashmap_fast.go: +0x3d9 fp=0xcbfa8 sp=0xcbf pc=0xbed9 main.modifyNotSafe(0xc) mainMap.go: +0x4a fp=0xcbfd8 sp=0xcbfa8 pc=0x4a1f1a runtime.goexit() C:/Go/src/runtime/asm_amd.s: +0x1 fp=0xcbfe0 sp=0xcbfd8 pc=0xcc1 created by main.main mainMap.go: +0x
对比之前《 Go实例讲解,并发编程-slice并发读写的线程安全性问题》,slice的数据结构在不安全的并发执行中是不会报错的,只是数据可能会出现丢失。
而这里的map的数据结构,是直接报错,所以在使用中就必须认真对待,否则整个程序是无法继续执行的。
所以也看出来,Go在对待线程安全性问题方面,对slice还是android 源码设计模式 pdf更加宽容的,对map则更加严格,这也是在并发编程时对我们提出了基本的要求。
将上面的代码稍微做些修改,对 data[i]=i 的前后增加上 muMap.Lock() 和 muMap.Unlock() ,也就保证了多线程并行的情况下,遇到冲突时有互斥锁的保证,避免出现线程安全性问题。
关于为什么会出现线程安全性问题,这里就不再详细讲解了,大家可以参考之前的两篇文章《 Go实例讲解,并发编程-slice并发读写的线程安全性问题》和《 Go实例讲解,并发编程-数字递增的线程安全性问题》。
这里,我们再来探讨一个问题,如何保证map的线程安全性?
上面我们是通过 muMap 这个互斥锁来保证的。
而Go语言有一个概念:“不要通过共享内存来进行通信,而应该通过通信来共享内存”,也就是利用channel来保证线程安全性。
那么,这又要怎么来做呢?下面是实例代码:
/** * 并发编程,map的线程安全性问题,使用channel的方式 */ package main import ( "time" "fmt" ) var dataCh map[int]int = make(map[int]int) var chMap chan int = make(chan int) func main() { // 并发启动的协程数量 max := time1 := time.Now().UnixNano() for i := 0; i < max; i++ { go modifyByChan(i) } // 处理channel的服务 chanServ(max) time2 := time.Now().UnixNano() fmt.Printf("data len=%d, time=%d", len(dataCh), (time2-time1)/) } func modifyByChan(i int) { chMap <- i } // 专门处理chMap的服务程序 func chanServ(max int) { for { i := <- chMap dataCh[i] = i if len(dataCh) == max { return } } }
数据填充的方式我们还是用1w个协程来做,只不过使用了chMap这个channel来做队列。
然后在 chanServ 函数中启动一个服务,专门来消费chMap这个队列,然后把数据给map赋值 dataCh[i]=i 。
从上面简单的对比中,我们还看不出太多的区别,我们还是可以得出下面一些
1 通过channel的方式,其实就是通过队列把并发执行的数据读写改成了串行化,以避免线程安全性问题;
2 多个协程交互的时候,可以通过依赖同一个 channel对象来进行数据的读写和传递,而不需要共享变量,可以参考之前的文章《 Go实例讲解,利用channel实现协程的互动-会聊天的Tom&Jerry》;
我们再来对比一下程序的执行效率。
使用互斥锁的方式,执行返回数据如下:
data len=, time=4
使用channel的方式,执行返回数据如下:
data len=, time=
可以看出,这种很简单的针对map并发读写的场景,通过互斥锁的方式比channel的方式要快很多,毕竟channel的方式增加了channel的读写操作,而且channel的串行化处理,效率上也会低一些。
所以,根据具体的情况,我们可以考虑优先用什么方式来实现。
优先使用互斥锁的场景:
1 复杂且频繁的数据读写操作,如:缓存数据;
2 应用中全局的共享数据,如:全局变量;
优先使用channel的场景:
1 协程之间局部传递共享数据,如:订阅发布模式;
2 统一的数据处理服务,如:库存更新+订单处理;
至此,我们已经通过3个Go实例讲解,知道在并发读写的情况下,如何搞定线程安全性问题,简单的数据结构就是int类型的安全读写,复杂的数据结构分别详细讲解了slice和map。在这次map的讲解中,还对比了互斥锁和channel的方式,希望大家能够对并发编程有更深入的理解。
goland mapåºå±åç
map æ¯Goè¯è¨ä¸åºç¡çæ°æ®ç»æï¼å¨æ¥å¸¸ç使ç¨ä¸ç»å¸¸è¢«ç¨å°ãä½æ¯å®åºå±æ¯å¦ä½å®ç°çå¢ï¼
æ»ä½æ¥è¯´golangçmapæ¯hashmapï¼æ¯ä½¿ç¨æ°ç»+é¾è¡¨çå½¢å¼å®ç°çï¼ä½¿ç¨æé¾æ³æ¶é¤hashå²çªã
golangçmapç±ä¸¤ç§éè¦çç»æï¼hmapåbmap(ä¸æä¸é½æ解é)ï¼ä¸»è¦å°±æ¯hmapä¸å å«ä¸ä¸ªæåbmapæ°ç»çæéï¼keyç»è¿hashå½æ°ä¹åå¾å°ä¸ä¸ªæ°ï¼è¿ä¸ªæ°ä½ä½ç¨äºéæ©bmap(å½ä½bmapæ°ç»æéçä¸è¡¨)ï¼é«ä½ç¨äºæ¾å¨bmapç[8]uint8æ°ç»ä¸ï¼ç¨äºå¿«éè¯éãç¶åä¸ä¸ªbmapå¯ä»¥æåä¸ä¸ä¸ªbmap(æé¾)ã
Golangä¸mapçåºå±å®ç°æ¯ä¸ä¸ªæ£å表ï¼å æ¤å®ç°mapçè¿ç¨å®é ä¸å°±æ¯å®ç°æ£è¡¨çè¿ç¨ãå¨è¿ä¸ªæ£å表ä¸ï¼ä¸»è¦åºç°çç»æä½æ两个ï¼ä¸ä¸ªå« hmap (a header for a go map)ï¼ä¸ä¸ªå« bmap (a bucket for a Go mapï¼é常å«å ¶bucket)ãè¿ä¸¤ç§ç»æçæ ·ååå«å¦ä¸æ示ï¼
hmap :
å¾ä¸æå¾å¤å段ï¼ä½æ¯ä¾¿äºç解mapçæ¶æï¼ä½ åªéè¦å ³å¿çåªæä¸ä¸ªï¼å°±æ¯æ 红çåæ®µï¼ bucketsæ°ç» ãGolangçmapä¸ç¨äºåå¨çç»ææ¯bucketæ°ç»ãèbucket(å³bmap)çç»ææ¯ææ ·çå¢ï¼
bucket ï¼
ç¸æ¯äºhmapï¼bucketçç»ææ¾å¾ç®åä¸äºï¼æ 红çå段ä¾ç¶æ¯âæ ¸å¿âï¼æ们使ç¨çmapä¸çkeyåvalueå°±åå¨å¨è¿éãâé«ä½åå¸å¼âæ°ç»è®°å½çæ¯å½åbucketä¸keyç¸å ³çâç´¢å¼âï¼ç¨åä¼è¯¦ç»åè¿°ãè¿æä¸ä¸ªå段æ¯ä¸ä¸ªæåæ©å®¹åçbucketçæéï¼ä½¿å¾bucketä¼å½¢æä¸ä¸ªé¾è¡¨ç»æãä¾å¦ä¸å¾ï¼
ç±æ¤çåºhmapåbucketçå ³ç³»æ¯è¿æ ·çï¼
èbucketåæ¯ä¸ä¸ªé¾è¡¨ï¼æ以ï¼æ´ä½çç»æåºè¯¥æ¯è¿æ ·çï¼
åå¸è¡¨çç¹ç¹æ¯ä¼æä¸ä¸ªåå¸å½æ°ï¼å¯¹ä½ ä¼ æ¥çkeyè¿è¡åå¸è¿ç®ï¼å¾å°å¯ä¸çå¼ï¼ä¸è¬æ åµä¸é½æ¯ä¸ä¸ªæ°å¼ãGolangçmapä¸ä¹æè¿ä¹ä¸ä¸ªåå¸å½æ°ï¼ä¹ä¼ç®åºå¯ä¸çå¼ï¼å¯¹äºè¿ä¸ªå¼ç使ç¨ï¼Golangä¹æ¯å¾æææã
Golangææ±å¾çå¼æç §ç¨éä¸å为äºï¼é«ä½åä½ä½ã
å¦å¾æ示ï¼èè²ä¸ºé«ä½ï¼çº¢è²ä¸ºä½ä½ã ç¶åä½ä½ç¨äºå¯»æ¾å½åkeyå±äºhmapä¸çåªä¸ªbucketï¼èé«ä½ç¨äºå¯»æ¾bucketä¸çåªä¸ªkeyãä¸æä¸æå°ï¼bucketä¸æ个å±æ§å段æ¯âé«ä½åå¸å¼âæ°ç»ï¼è¿éåçå°±æ¯èè²çé«ä½å¼ï¼ç¨æ¥å£°æå½åbucketä¸æåªäºâkeyâï¼ä¾¿äºæç´¢æ¥æ¾ã éè¦ç¹å«æåºçä¸ç¹æ¯ï¼æ们mapä¸çkey/valueå¼é½æ¯åå°åä¸ä¸ªæ°ç»ä¸çãæ°ç»ä¸ç顺åºæ¯è¿æ ·ç:
并ä¸æ¯key0/value0/key1/value1çå½¢å¼ï¼è¿æ ·åç好å¤æ¯ï¼å¨keyåvalueçé¿åº¦ä¸åçæ¶åï¼å¯ 以æ¶é¤padding(å å对é½)带æ¥ç空é´æµªè´¹ ã
ç°å¨ï¼æ们å¯ä»¥å¾å°Goè¯è¨mapçæ´ä¸ªçç»æå¾äºï¼(hashç»æçä½ä½ç¨äºéæ©æKVæ¾å¨bmapæ°ç»ä¸çåªä¸ä¸ªbmapä¸ï¼é«ä½ç¨äºkeyçå¿«éé¢è§ï¼ç¨äºå¿«éè¯é)
mapçæ©å®¹
å½ä»¥ä¸çåå¸è¡¨å¢é¿çæ¶åï¼Goè¯è¨ä¼å°bucketæ°ç»çæ°éæ©å ä¸åï¼äº§çä¸ä¸ªæ°çbucketæ°ç»ï¼å¹¶å°æ§æ°ç»çæ°æ®è¿ç§»è³æ°æ°ç»ã
å è½½å å
å¤ææ©å çæ¡ä»¶ï¼å°±æ¯åå¸è¡¨ä¸çå è½½å å(å³loadFactor)ã
å è½½å åæ¯ä¸ä¸ªéå¼ï¼ä¸è¬è¡¨ç¤ºä¸ºï¼æ£åå å«çå ç´ æ° é¤ä»¥ ä½ç½®æ»æ°ãæ¯ä¸ç§â产çå²çªæºä¼âåâ空é´ä½¿ç¨âç平衡ä¸æä¸ï¼å è½½å åè¶å°ï¼è¯´æ空é´ç©ºç½®çé«ï¼ç©ºé´ä½¿ç¨çå°ï¼ä½æ¯å è½½å åè¶å¤§ï¼è¯´æ空é´å©ç¨çä¸å»äºï¼ä½æ¯â产çå²çªæºä¼âé«äºã
æ¯ç§åå¸è¡¨çé½ä¼æä¸ä¸ªå è½½å åï¼æ°å¼è¶ è¿å è½½å åå°±ä¼ä¸ºåå¸è¡¨æ©å®¹ã
Golangçmapçå è½½å åçå ¬å¼æ¯ï¼mapé¿åº¦ / 2^B(è¿æ¯ä»£è¡¨bmapæ°ç»çé¿åº¦ï¼Bæ¯åçä½ä½çä½æ°)éå¼æ¯6.5ãå ¶ä¸Bå¯ä»¥ç解为已æ©å®¹ç次æ°ã
å½Goçmapé¿åº¦å¢é¿å°å¤§äºå è½½å åæéçmapé¿åº¦æ¶ï¼Goè¯è¨å°±ä¼å°äº§çä¸ä¸ªæ°çbucketæ°ç»ï¼ç¶åææ§çbucketæ°ç»ç§»å°ä¸ä¸ªå±æ§å段oldbucketä¸ã注æï¼å¹¶ä¸æ¯ç«å»ææ§çæ°ç»ä¸çå ç´ è½¬ä¹å°æ°çbucketå½ä¸ï¼èæ¯ï¼åªæå½è®¿é®å°å ·ä½çæ个bucketçæ¶åï¼ä¼æbucketä¸çæ°æ®è½¬ç§»å°æ°çbucketä¸ã
å¦ä¸å¾æ示ï¼å½æ©å®¹çæ¶åï¼Goçmapç»æä½ä¸ï¼ä¼ä¿åæ§çæ°æ®ï¼åæ°çæçæ°ç»
ä¸é¢é¨å代表æ§çææ°æ®çbucketï¼ä¸é¢é¨å代表æ°çæçæ°çbucketãèè²ä»£è¡¨åææ°æ®çbucketï¼æ©é»è²ä»£è¡¨ç©ºçbucketã
æ©å®¹æ¶map并ä¸ä¼ç«å³ææ°æ°æ®åè¿ç§»ï¼èæ¯å½è®¿é®åæ¥æ§bucketçæ°æ®çæ¶åï¼æææ§æ°æ®åè¿ç§»ï¼å¦ä¸å¾ï¼
注æï¼è¿é并ä¸ä¼ç´æ¥å é¤æ§çbucketï¼èæ¯æåæ¥çå¼ç¨å»æï¼å©ç¨GCæ¸ é¤å åã
mapä¸æ°æ®çå é¤
å¦æç解äºmapçæ´ä½ç»æï¼é£ä¹æ¥æ¾ãæ´æ°ãå é¤çåºæ¬æ¥éª¤åºè¯¥é½å¾æ¸ æ¥äºãè¿éä¸åèµè¿°ã
å¼å¾æ³¨æçæ¯ï¼æ¾å°äºmapä¸çæ°æ®ä¹åï¼é对keyåvalueåå«åå¦ä¸æä½ï¼
1
2
3
4
1ãå¦æ``key``æ¯ä¸ä¸ªæéç±»åçï¼åç´æ¥å°å ¶ç½®ä¸ºç©ºï¼çå¾ GCæ¸ é¤ï¼
2ãå¦ææ¯å¼ç±»åçï¼åæ¸ é¤ç¸å ³å åã
3ãåçï¼å¯¹``value``åç¸åçæä½ã
4ãæåækey对åºçé«ä½å¼å¯¹åºçæ°ç»index置为空ã
map在golang的底层实现和源码分析
在Golang 1..2版本中,map的底层实现由两个核心结构体——hmap和bmap(此处用桶来描述)——构建。初始化map,如`make(map[k]v, hint)`,会创建一个hmap实例,包含map的所有信息。makemap函数负责创建hmap、计算B值和初始化桶数组。
Golang map的高效得益于其巧妙的设计:首先,key的hash值的后B位作为桶索引;其次,key的hash值的前8位决定桶内结构体的数组索引,包括tophash、key和value;tophash数组还用于存储标志位,当桶内元素为空时,标志位能快速识别。读写删除操作充分利用了这些设计,包括更新、新增和删除key-value对。
删除操作涉及到定位key,移除地址空间,更新桶内tophash的标志位。而写操作,虽然mapassign函数返回value地址但不直接写值,实际由编译器生成的汇编指令提高效率。扩容和迁移机制如sameSizeGrow和biggerSizeGrow,针对桶利用率低或桶数组满的情况,通过调整桶结构和数组长度,优化查找效率。
evacuate函数负责迁移数据到新的桶区域,并清理旧空间。最后,虽然本文未详述,但订阅"后端云"公众号可获取更多关于Golang map底层实现的深入内容。
go map and slice --
golangæ¯å¼ä¼ éï¼ä»ä¹æ åµä¸é½æ¯å¼ä¼ éé£ä¹ï¼å¦æç»æä¸ä¸å«æéï¼åç´æ¥èµå¼å°±æ¯æ·±åº¦æ·è´ï¼
å¦æç»æä¸å«ææéï¼å æ¬èªå®ä¹æéï¼ä»¥åsliceï¼mapç使ç¨äºæéçå 置类åï¼ï¼åæ°æ®æºåæ·è´ä¹é´å¯¹åºæéä¼å ±åæååä¸åå åï¼è¿æ¶æ·±åº¦æ·è´éè¦ç¹å«å¤çãå 为å¼ä¼ éåªæ¯ææéæ·è´äº
mapæºç :
/golang/go/blob/a7acf9afbdcfabfdf4/src/runtime/map.go
mapæéè¦ç两个ç»æä½ï¼hmap å bmap
å ¶ä¸ hmap å å½äºåå¸è¡¨ä¸æ°ç»çè§è²ï¼ bmapå å½äºé¾è¡¨çè§è²ã
å ¶ä¸ï¼å个bucketæ¯ä¸ä¸ªå«bmapçç»æä½.
Each bucket contains up to 8 key/elem pairs.
And the low-order bits of the hash are used to select a bucket. Each bucket contains a few high-order bits of each hash to distinguish the entries within a single bucket.
hashå¼çä½ä½ç¨æ¥å®ä½bucketï¼é«ä½ç¨æ¥å®ä½bucketå é¨çkey
æ ¹æ®ä¸é¢bmapç注éå /golang/go/blob/go1..8/src/cmd/compile/internal/gc/reflect.go ï¼
æ们å¯ä»¥æ¨åºbmapçç»æå®é æ¯
注æï¼å¨åå¸æ¡¶ä¸ï¼é®å¼ä¹é´å¹¶ä¸æ¯ç¸é»æåçï¼èæ¯é®æ¾å¨ä¸èµ·ï¼å¼æ¾å¨ä¸èµ·ï¼æ¥åå°å 为é®å¼ç±»åä¸åè产ççä¸å¿ è¦çå å对é½
ä¾å¦map[int]int8ï¼å¦æ key/elem/key/elemè¿æ ·åæ¾ï¼é£ä¹int8ç±»åçå¼å°±è¦padding 7个åèå ±bits
æ´å¤å¯åè
/p/
/articles/
å æ¤ï¼sliceãmapä½ä¸ºåæ°ä¼ éç»å½æ°å½¢åï¼å¨å½æ°å é¨çæ¹å¨ä¼å½±åå°åsliceãmap
Go 语言入门 2-集合(map)的特性及实现原理
在 Go 语言的世界里,map 是一种独特的数据结构,它以键值对的形式存储数据,以其高效性和独特的哈希管理机制著称。Go 语言的 map 实现由 hmap 结构管理哈希桶数组,而桶的内部结构由 bmap 定义,保证了键的唯一性并提供了近乎瞬时的 O(1) 查找性能。
map 的创建方式有两种:一是通过字面量初始化,二是通过 make 函数,这为灵活性提供了保障。其基本操作包括:通过计算键的哈希值获取索引,对桶进行查找、添加、更新或删除元素。在处理哈希冲突时,Go 采用了一种名为拉链法的策略,当桶满时,会创建新的溢出桶,并通过 next 指针将它们串联起来。然而,随着元素的增长,查询效率会逐渐降低,因此,Go 通过负载因子这一指标来监控冲突,当达到预设阈值(例如,Go 6.5 版本的 0.,Redis 1 和 Java 0. 不同)时,会触发 rehash 过程。当溢出桶数量达到 \(2^{ }\) 时,也会自动进行调整。
rehash 是一个细致的分步操作,它逐步地将旧桶的数据迁移到新桶,确保数据的一致性。这个过程结束后,旧的哈希桶会被释放,以保持内存的高效利用。深入理解 map 的这些核心原理,将有助于你在 Go 的开发旅程中游刃有余。
如果你在实际应用中遇到 map 相关的挑战,别犹豫,我们欢迎你在评论区分享你的问题,让我们共同探索和学习。如果你想了解更多关于 Go 语言的实用技巧,别忘了关注我们的公众号「Python 学习爱好者」,那里有丰富的编程资源和成长社区,期待你的加入。
golang map 源码解读(8问)
map底层数据结构为hmap,包含以下几个关键部分:
1. buckets - 指向桶数组的指针,存储键值对。
2. count - 记录key的数量。
3. B - 桶的数量的对数值,用于计算增量扩容。
4. noverflow - 溢出桶的数量,用于等量扩容。
5. hash0 - hash随机值,增加hash值的随机性,减少碰撞。
6. oldbuckets - 扩容过程中的旧桶指针,判断桶是否在扩容中。
7. nevacuate - 扩容进度值,小于此值的已经完成扩容。
8. flags - 标记位,用于迭代或写操作时检测并发场景。
每个桶数据结构bmap包含8个key和8个value,以及8个tophash值,用于第一次比对。
overflow指向下一个桶,桶与桶形成链表存储key-value。
结构示意图在此。
map的初始化分为3种,具体调用的函数根据map的初始长度确定:
1. makemap_small - 当长度不大于8时,只创建hmap,不初始化buckets。
2. makemap - 当长度参数为int时,底层调用makemap。
3. makemap - 初始化hash0,计算对数B,并初始化buckets。
map查询底层调用mapaccess1或mapaccess2,前者无key是否存在的bool值,后者有。
查询过程:计算key的hash值,与低B位取&确定桶位置,获取tophash值,比对tophash,相同则比对key,获得value,否则继续寻找,直至返回0值。
map新增调用mapassign,步骤包括计算hash值,确定桶位置,比对tophash和key值,插入元素。
map的扩容有两种情况:当count/B大于6.5时进行增量扩容,容量翻倍,渐进式完成,每次最多2个bucket;当count/B小于6.5且noverflow大于时进行等量扩容,容量不变,但分配新bucket数组。
map删除元素通过mapdelete实现,查找key,计算hash,找到桶,遍历元素比对tophash和key,找到后置key,value为nil,修改tophash为1。
map遍历是无序的,依赖mapiterinit和mapiternext,选择一个bucket和offset进行随机遍历。
在迭代过程中,可以通过修改元素的key,value为nil,设置tophash为1来删除元素,不会影响遍历的顺序。