前言
关于更多map底层原理剖析,请点击这一篇博文Golang底层原理剖析之map
map介绍
- map 读取某个值时 - 返回结果可以为 value,bool 或者 value。注意后者,在key不存在时,会返回value对应类型的默认值
- map 的 range 方法需要注意 - key,value 或者 key。注意后者,可以和slice的使用结合起来
- map 的底层相关的实现 - 串联 初始化、赋值、扩容、读取、删除 这五个常见实现的背后知识点,详细参考示例代码链接与源码
使用示例
package main import "fmt" func mapper() { // var mp map[string]int 不会初始化 mp := make(map[string]int) mp["Tom"] = 10 age, ok := mp["Tom"] fmt.Println(age, ok) age1 := mp["Tom"] age2 := mp["Tom2"] fmt.Println(age1, age2) for key, value := range mp { fmt.Println(key, value) } for key := range mp { fmt.Println(key, mp[key]) } var data = []int{1, 3, 5} for key := range data { fmt.Println(key, data[key]) } } // go tool compile -S map.go func mapCompile() { m := make(map[string]int, 9) key := "test" m[key] = 1 _, ok := m[key] if ok { delete(m, key) } } /* 源码文件:runtime/map.go 初始化:makemap 1. map中bucket的初始化 makeBucketArray 2. overflow 的定义为哈希冲突的值,用链表法解决 赋值:mapassign 1. 不支持并发操作 h.flags&hashWriting 2. key的alg算法包括两种,一个是equal,用于对比;另一个是hash,也就是哈希函数 3. 位操作 h.flags ^= hashWriting 与 h.flags &^= hashWriting 4. 根据hash找到bucket,遍历其链表下的8个bucket,对比hashtop值;如果key不在map中,判断是否需要扩容 扩容:hashGrow 1. 扩容时,会将原来的 buckets 搬运到 oldbuckets 读取:mapaccess 1. mapaccess1_fat 与 mapaccess2_fat 分别对应1个与2个返回值 2. hash 分为低位与高位两部分,先通过低位快速找到bucket,再通过高位进一步查找,最后对比具体的key 3. 访问到oldbuckets中的数据时,会迁移到buckets 删除:mapdelete 1. 引入了emptyOne与emptyRest,后者是为了加速查找 */
map常见使用过程中的问题
- map 的 range 操作 - key、value 都是值复制
- map 如何保证按key的某个顺序遍历? - 分两次遍历,第一次取出所有的key并排序;第二次按排序后的key去遍历(这时你可以思考封装map和slice到一个结构体中)?
- map 的使用上,有什么要注意的? - 遍历时,尽量只修改或删除当前key,操作非当前的key会带来不可预知的结果
- 从 map 的设计上,我们可以学到 - Go语言对map底层的hmap做了很多层面的优化与封装,也屏蔽了很多实现的细节,适用于绝大多数的场景;而少部分有极高性能要求的场景,就需要深入到hmap中的相关细节。
问题示例
package main import ( "fmt" ) func main() { mapAddr() mapModify() mapReplace() mapSet() mapMap() } func mapAddr() { var mp = map[int]int{1: 2, 2: 3, 3: 4} fmt.Printf("%p\n", &mp) fmt.Println("map range 1 start") // Tip 注意range中的k是不能保证顺序的 for k, v := range mp { // Tip: k,v 是为了遍历、另外开辟内存保存的一对变量,不是map中key/value保存的地址 fmt.Println(&k, &v) // Tip: 修改k/v后,不会生效 k++ v++ // Tip 如果在这里加上一个delete操作,在k=1时会删除k=2的元素,然后就直接跳过元素2,遍历到元素3 // map的delete是安全的,不会存在race等竞争问题,这里用到的k,v是值复制,删除是针对hmap的操作 // delete(mp, k) // 编译错误,value不可寻址,因为这个在内部是频繁变化的 // fmt.Printf("%p", &mp[k]) } fmt.Println(mp) fmt.Println("map range 1 end") } func mapModify() { // Tip: 如果map最终的size比较大,就放到初始化的make中,会减少hmap扩容带来的内容重新分配 //var mp2 = make(map[int]int,1000) var mp2 = make(map[int]int) mp2[1] = 2 mp2[2] = 3 mp2[3] = 4 fmt.Println("map range 2 start") for k, v := range mp2 { // Tip: 在range的过程中,如果不断扩容,何时退出是不确定的,是随机的,和是否需要sleep无关 mp2[k+1] = v + 1 // time.Sleep(10 * time.Millisecond) } fmt.Println(len(mp2)) fmt.Println("map range 2 end") } // https://stackoverflow.com/questions/45132563/idiomatic-way-of-renaming-keys-in-map-while-ranging-over-the-original-map func mapReplace() { o := make(map[string]string) // original map r := make(map[string]string) // replacement map original -> destination keys o["a"] = "x" o["b"] = "y" r["a"] = "1" r["b"] = "2" fmt.Println(o) // -> map[a:x b:y] // Tip: 因为o的k-v是在不断增加的,所以遍历何时结束是不确定的 // 此时k可能为"1"或者"2",对应的r[k]不存在, //此时r这个map会创建key为1或2,value返回默认值空字符串 "", //所以结果会多一个key(r[k])为"",value为"x"或"y"的异常值 for k, v := range o { o[r[k]] = v //想要实现<1,x><2,y>,但是结果是map[:x 1:x 2:y] } // Tip: 到这里,也许你会好奇,为什么每次运行的结果会不一致呢?map[:x 1:x 2:y]或者map[:y 1:x 2:y] // 1. 首先,我们要了解一点,遍历这个工作在hmap中是通过buckets进行的 // 2. 因为多次运行的结果不一致,说明每一次运行时,分配的bucket是有随机的 // 3. 仔细查看hmap这个结构体,我们不难发现hash0这个随机值,确认其对分配bucket的hash计算带来的影响 // 4. 因为有hash0这个随机种子,所以遍历range是随机的 delete(o, "a") delete(o, "b") fmt.Println(o) } func mapSet() { // 空struct是不占用空间的,将struct{}作为value,就可以作为set集合来用了 var mp = make(map[string]struct{}) _, ok := mp["test"] fmt.Println(ok) } func mapMap() { mp := make(map[string]map[string]string) //date:=mp["test"] //date["test"]="test" panic //make初始化的时候只会初始化第一层的map,而第二层的map是不会初始化的 //一定要对第二层的map进行初始化 mp["test"] = make(map[string]string) makeDate := mp["test"] makeDate["test"] = "test" fmt.Println(mp) }