16|复合数据类型:原生map类型的实现机制是怎样的?
16|复合数据类型:原生map类型的实现机制是怎样的?
讲述:Tony Bai
时长22:57大小20.97M
什么是 map 类型?
map 变量的声明和初始化
map 的基本操作
map 变量的传递开销
map 的内部实现
初始状态
map 扩容
map 与并发
小结
思考题
赞 45
提建议
精选留言(59)
- 罗杰2021-11-17还想深入研究 map 的小伙伴,可以去研究这些博客: https://www.qcrao.com/2019/05/22/dive-into-go-map/ https://qcrao.com/2020/05/06/dive-into-go-sync-map/ https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/共 9 条评论33
- 用0和1改变自己2021-11-17可以用一个有序结构存储key,如slice,然后for这个slice,用key获取值。资料来源至:https://go.dev/blog/maps
作者回复: ✅
共 2 条评论26 - ddh2022-04-25请问一下老师, map 元素不能寻址, 是因为动态扩容, 那么切片不也有动态扩容吗。 为什么切片元素可以寻址呢? 难道切片动态扩容之后, 它的元素地址不会变吗
作者回复: 好问题! 关于这个问题,官方没有明确说明。 我觉得之所以map element是不可寻址的,还是复杂性和安全性的考虑。 Go slice和map都是复合数据类型,但两者的内部实现复杂度却远远不同。map要远复杂于slice。 slice的元素的地址要么在要么在原底层数组中,要么在扩容后的新数组中。并且slice扩容是一次完成的。 但map扩容是“蚂蚁搬家”,element位置复杂一些,可能在原bucket中、可能在overflow bucket中,也可能在新扩容的bucket中。 此外,一旦暴露map element的地址,比如我们用一个栈上的指针变量引用了该地址,当发生element移动时,go就要考虑连带变更这些栈上指针变量的指向,这些变更操作在每个map操作中都会导致运行时消耗的增加。 至于安全性,map有着一个复杂内部结构,这个结构环环相扣。一旦map element地址被暴露,通过该地址不仅可以修改value值,还可能会对map内部结构造成破坏,这种破坏将导致整个map工作不正常。而slice的element即便被hack,改掉的也只是元素值。
共 3 条评论11 - thomas2022-04-12go团队为什么要故意把map的遍历设置为随机?
作者回复: 这一话题要追溯到Go 1.0版本发布的时候,从Go 1.0版本的发布文档- https://go.dev/doc/go1中,我们能找到如下内容: The old language specification did not define the order of iteration for maps, and in practice it differed across hardware platforms. This caused tests that iterated over maps to be fragile and non-portable, with the unpleasant property that a test might always pass on one machine but break on another. In Go 1, the order in which elements are visited when iterating over a map using a for range statement is defined to be unpredictable, even if the same loop is run multiple times with the same map. Code should not assume that the elements are visited in any particular order. This change means that code that depends on iteration order is very likely to break early and be fixed long before it becomes a problem. Just as important, it allows the map implementation to ensure better map balancing even when programs are using range loops to select an element from a map. 翻译过来,大致意思就是:1.0版本之前的语言规范中没有规定map的迭代次序,从而导致在实践中的一些糟糕的开发运行体验。于是Go团队选 择故意在map中加入随机迭代功能,这样一旦出现开发人员依赖key迭代顺序的错误行为,这一行为导致的问题在开发和测试早期就能被及时发现,而不会出现在生产运行环境中导致更大的危害。
8 - Darren2021-11-17可以参考java的LinkedHashMap,能实现插入有序或者访问有序,就是使用额外的链表来保存顺序。 go 中可以基于container/list来实现。github上现成的功能。 https://github.com/elliotchance/orderedmap6
- ddh2021-11-17把key存到有序切片中,用切片遍历
作者回复: 是一个可行方法。
共 2 条评论4 - Geek_244c462022-05-26map的key和value的值,是否可以为null
作者回复: 用代码回答你的问题:) // testnilasmapkey.go package main func main() { var m = make(map[*int]*int) m[nil] = nil println(m) println(len(m)) var b = 5 m[nil] = &b p := m[nil] println(*p) } 0xc0000466b0 1 5 nil作为value不新鲜。但nil可作为key可是很多人都不知道的。
3 - flexiver2022-04-21老师,想请问一下,hmap这个结构中的extra字段, 在key和value都不是指针的情况下,会存储所有的overflow bucket的指针,里面有提到一个内联,这个内联是什么意思?以及为什么当key和value都不是指针的情况下,会将bucket中的overflow指针全部放到extra字段存储
作者回复: 在Go项目源码(Go 1.17版本)的 src/cmd/compile/internal/reflectdata/reflect.go中,func MapBucketType(t *types.Type) *types.Type>这个函数实现中,我们可以勾勒出一个bucket桶的结构: tophash keys values overflow pointer 不过这个overflow pointer有两种情况: // If keys and elems have no pointers, the map implementation // can keep a list of overflow pointers on the side so that // buckets can be marked as having no pointers. // Arrange for the bucket to have no pointers by changing // the type of the overflow field to uintptr in this case. // See comment on hmap.overflow in runtime/map.go. otyp := types.Types[types.TUNSAFEPTR] if !elemtype.HasPointers() && !keytype.HasPointers() { otyp = types.Types[types.TUINTPTR] } 当key和value中都没有指针时,比如map[int]int。此时考虑到gc优化,编译器将overflow的类型设置为uintptr。 uintptr是一个整型,无法被GC识别,这样一来uintptr指向的overflow bucket就没有指向它的指针,这样gc就会将overflow bucket视为unrea chable的mem块而将其释放掉。为了避免这种情况,hmap中的extra此时就会指向上面这类bucket的overflow bucket,保证key和value中都不含指针时,overflow bucket依旧可以不被gc。
3 - 文经2022-01-05我看了《Go语言实践》,《Go专家编程》,《Go语言底层原理剖析》这几本书对map的描述,对比才发现白老师讲得最清晰,在封装和细节方面拿捏得最好,大大地赞👍 剩下不清楚的地方就只能自己看源码了。
作者回复: 过奖。其实原理这东西最终还是要落到代码上。而且不同版本实现也有差异。要想真正把原理吃透,还得自己多过几遍代码
3 - 多选参数2021-11-22突然想到之前碰到的一个问题,就是golang 结构体作为map的元素时,不能够直接赋值给结构体的某个字段。这个也有“Go 不允许获取 map 中 value 的地址”的原因在嘛?假如这样的话,那么为什么读结构体中的某个字段是可以的?
作者回复: 读操作得到的是值的一个copy
共 2 条评论3 - 不负青春不负己🤘2021-11-19可以把key 赋值到变量,使用sort 排序,在遍历
作者回复: ✅
3 - 高雪斌2021-11-17func doIteration(m map[int]int) { mu.RLock() defer mu.RUnlock() keys := []int{} for k := range m { keys = append(keys, k) } sort.SliceStable(keys, func(x, y int) bool { return x < y }) for _, k := range keys { fmt.Printf("[%d, %d] ", k, m[k]) } fmt.Println() } func doWrite(m map[int]int) { mu.Lock() defer mu.Unlock() for k, v := range m { m[k] = v + 1 } } ==> 对并发示例代码的稳定排序输出 (原例1000次输出太多,输出前10个作为说明): [1, 11] [2, 12] [3, 13] [1, 12] [2, 13] [3, 14] [1, 13] [2, 14] [3, 15] [1, 14] [2, 15] [3, 16] [1, 15] [2, 16] [3, 17] [1, 16] [2, 17] [3, 18] [1, 17] [2, 18] [3, 19] [1, 18] [2, 19] [3, 20] [1, 19] [2, 20] [3, 21] [1, 20] [2, 21] [3, 22] 。。。展开
作者回复: 不错的思路。
共 2 条评论2 - Geek_f654de2022-02-25思考题: 1.把map的key拿出来,放到切片里 2.对切片进行排序 3.取出切片中元素对应的value
作者回复: ✅
1 - Casper2021-12-02老师,请问一下如果是因为overflow bucket过多导致的"扩容", 是否可以理解为这个hash函数的算法不太合理,导致大部分的key都分配到了一个bucket中,是否可以通过修改hash算法来重新hash一遍呢?
作者回复: go map底层的hash函数要考虑通用性。谁也不能预测用户会使用什么样的key数据。
1 - lesserror2021-11-19Tony Bai 老师,麻烦有时间回复一下以下几个问题: 1. 文中说: “map 所用的 hash 函数也存放在 maptype.key.alg.hash(key, hmap.hash0) 中。” 后面的这一串:“maptype.key.alg.hash(key, hmap.hash0) ” 是什么? 看的有点懵。 2. 不管是使用字面值 还是用 make函数初始化 map类型变量,打印变量名,都是输出的 map[] 而不是 nil ,这个输出是正常的吗?展开
作者回复: 1. 引用层次太深,所以用了伪代码形式的简写。就是说hash函数存放在maptype类型中的key字段的alg字段中的hash字段中,key是hash函数的第一个参数,是待hash的字段。第二个参数是种子(hmap.hash0就是种子)。go 1.17中这块的组成发生了一些变化。这个hash函数直接放在maptype的hasher字段中了。但原理应该没有变。 2. var m map[string]int // m = nil 你说的是这里么?这里后面注释中不是打印输出,而是指 m为nil。
共 4 条评论1 - AlexWillBeGood2021-11-18老师,有一点我不明白,为什么kv 紧邻存储的时候一定需要Padding?
作者回复: 看完第17讲,你就有思路了。
共 4 条评论1 - Geek_0b92d92021-11-17老师好。我是 go1.16.2 版本 var m map[string]int 定义后, 打印 m , 是 map[],不是 nil
作者回复: var m map[string]int // m = nil 注释中的m = nil 是提示m的值为nil,不是打印输出。
共 2 条评论1 - return2021-11-17老师讲的太好了,1
- aoe2021-11-17使用链表存储可以得到有序的Map。有了描述符,再也不用担心传递性能问题了!原来多个bucket是为了降低Hash冲突,虽然一个bucket和多个bucket在查找Key时时间复杂度都是O(1),但一个Bucket遇到Hash冲突的可能性要比多个高出很多。1
- 邹志鹏.Joey ⁷⁷...2023-01-18 来自广东既切片之后, 应该是 "继切片之后"?
作者回复: 👍。稍后让编辑老师修改一下这个typo。