地图数据结构的一般概念是它是一个键值对的集合。并没有提到“有序”或“排序”。
维基百科定义:
在计算机科学中,关联数组、映射、符号表或字典是由一组(key, value)对组成的抽象数据类型,以使集合中每个可能的键只出现一次。
地图是计算机科学中最有用的数据结构之一,因此Go将其作为内置类型提供。然而,语言规范仅指定了一般地图(Map types):
地图是一组无序的元素,其中元素类型称为元素类型,由另一种类型的唯一键索引,称为键类型。未初始化地图的值为nil。
请注意,语言规范不仅省略了“有序”或“排序”这些词,而且明确指出了相反的意思:“无序”。但是为什么呢?因为这给运行时实现映射类型提供了更大的自由度。语言规范允许使用任何映射实现,如哈希映射、树映射等。请注意,当前(和以前)的Go版本使用哈希映射实现,但您不需要知道这一点才能使用它。
关于这个问题,博客文章
Go maps in action是必读的。
在Go 1之前,当地图没有改变时,运行时会在多次迭代键/条目时以相同的顺序返回键。请注意,如果地图被修改,此顺序可能会发生更改,因为实现可能需要重新散列以容纳更多条目。人们开始依赖相同的迭代顺序(当地图未更改时),因此从Go 1开始,运行时有意随机化地图迭代顺序,以引起开发人员的注意,表明顺序未定义,不能依赖。
那么该怎么办呢?
如果你需要一个按插入顺序或键类型自然顺序或任意顺序排序的数据集(无论是键值对集合还是其他任何东西),map
都不是正确的选择。如果你需要预定义顺序,那么切片(和数组)就是你的朋友。如果你需要能够通过预定义的键查找元素,则可以另外建立一个从切片到 map 的映射,以便快速查找元素。
你可以先构建 map
,然后按正确的顺序构建切片,也可以先构建切片,然后从中构建 map
,完全取决于你自己。
上述的 Go maps in action 博客文章有一节专门讲解 迭代顺序:
使用范围循环迭代映射时,迭代顺序未指定并且不能保证从一次迭代到下一次迭代相同。自Go 1以来,运行时会随机化映射迭代顺序,因为程序员依赖先前实现的稳定迭代顺序。如果需要稳定的迭代顺序,则必须维护一个指定该顺序的单独数据结构。此示例使用单独的已排序键片段以按键顺序打印
map[int]string
:
import "sort"
var m map[int]string
var keys []int
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
P.S.:
尽管看起来似乎没有表现出相同的行为。
在 Go Playground 上,你看到的“相同迭代顺序”实际上是因为应用程序/代码的输出被缓存了。一旦执行新的、但唯一的代码,其输出就会被保存为新的输出。一旦执行相同的代码,保存的输出就会被呈现,而不会再次运行代码。因此,你看到的不是相同的迭代顺序,而是完全相同的输出,而没有执行任何代码。
P.S. #2
虽然使用
for range
迭代顺序是“随机”的,但标准库中有一些值得注意的例外情况,它们按排序顺序处理映射,即
encoding/json
、
text/template
、
html/template
和
fmt
包。有关更多详细信息,请参见
In Golang, why are iterations over maps random?
O(1)
算法复杂度,就需要做出牺牲。 - JoeG