前言
关于更多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)
}