【Golang】如何有序地遍历 Map

8,921 阅读2分钟

原生的 Map 为什么无序?

我们在遍历 Map 的打印的时候,会注意到内容是无序的。

了解 Map 底层实现原理的同学知道,遍历 Map 时,是按顺序遍历 bucket ,然后再按顺序遍历 bucket 中 的 key(bucket 里面是个链表)。然而,Map 在扩容时,key 会发生迁移,从旧 bucket 迁移到新的 bucket,这种情况下,是做不到有序遍历的。

同时,Go 官方直接在遍历开始,使用 fastrand 随机选一个 buckct 作为起始桶。

有序遍历的思路

  1. 思路一(实现快):将 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])
	}
}
  1. 思路二(一劳永逸):利用官方库里的 list(链表) 封装一个结构体,实现一个有序的 K-V 存储结构,在里面维护一个 keys 的 list。

    为什么使用 list 而不是参考思路一用 slice 来保证有序?因为 list 可以实现 insertBeforeinsertAfter 类似的方法,而 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之类的数组如何实现。

参考文献

map 实现原理

github.com/elliotchanc…