原生的 Map 为什么无序?
我们在遍历 Map
的打印的时候,会注意到内容是无序的。
了解 Map
底层实现原理的同学知道,遍历 Map
时,是按顺序遍历 bucket
,然后再按顺序遍历 bucket
中 的 key(bucket
里面是个链表)。然而,Map
在扩容时,key 会发生迁移,从旧 bucket
迁移到新的 bucket
,这种情况下,是做不到有序遍历的。
同时,Go 官方直接在遍历开始,使用 fastrand
随机选一个 buckct 作为起始桶。
有序遍历的思路
- 思路一(实现快):将
Map
中的 key 拿出来,放入slice
中做排序
func main() {
m := make(map[string]string)
m["name"] = "Bob"
m["age"] = "25"
m["gender"] = "male"
keys := make([]string, 0)
for k := range m {
keys = append(keys, k)
}
//排序 key
sort.Strings(keys)
for _, key := range keys {
println(m[key])
}
}
-
思路二(一劳永逸):利用官方库里的
list(链表)
封装一个结构体,实现一个有序的 K-V 存储结构,在里面维护一个 keys 的 list。为什么使用
list
而不是参考思路一用slice
来保证有序?因为list
可以实现insertBefore
、insertAfter
类似的方法,而slice
实现相对复杂。参考现在一个比较流行的库注释讲解下:github.com/elliotchanc…
package orderedmap
import "container/list"
//......更多细节请看源码
//OrderedMap 核心数据结构
type OrderedMap struct {
//存储 k-v,使用 *list.Element 当做 value 是利用 map O(1) 的性能找到 list 中的 element
kv map[interface{}]*list.Element
//按顺序存储 k-v,保证插入、删除的时间复杂度O(1)
ll *list.List
}
//......更多细节请看源码
总结
Golang 中 Map
之所以放弃有序的遍历,也是出于性能和复杂度的考虑。这也是为什么上面的思路都或多或少地都比原生Map
使用起来复杂了许多。有兴趣的同学还可以了解PHP
之类的数组如何实现。