如何按插入顺序迭代映射?

26

我有一个作为地图的导航栏:

var navbar = map[string]navbarTab{
}

navbarTab 拥有不同的属性、子项等时,我尝试渲染导航栏(使用 for tabKey := range navbar),但它以随机顺序显示。我知道range在运行时会随机排序,但似乎没有办法获得按插入顺序排序的键的有序列表或迭代。

这里是播放链接: http://play.golang.org/p/nSL1zhadg5,尽管它似乎没有展示相同的行为。

如何在不破坏插入顺序的情况下遍历此映射?


3
据我最后一次检查,Go语言中的映射(maps)实际上并不记住插入顺序。跟踪顺序需要承担相当大的开销。 - user2357112
6
@Mark: 为什么你认为会出现这种情况?这是标准哈希映射行为。 - Arjan
1
你可以使用“切片”来跟踪插入顺序。 - Akavall
2
“这似乎是荒谬的,这是不可能的。”但是,要实现至少一种插入/查找/删除操作的 O(1) 算法复杂度,就需要做出牺牲。 - JoeG
2
随机化密钥顺序是一个有意的决定,并被认为是一种特性。http://nathanleclaire.com/blog/2014/04/27/a-surprising-feature-of-golang-that-colored-me-impressed/ - 425nesp
显示剩余3条评论
2个回答

39

地图数据结构的一般概念是它是一个键值对的集合。并没有提到“有序”或“排序”。

维基百科定义:

在计算机科学中,关联数组、映射、符号表或字典是由一组(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/jsontext/templatehtml/templatefmt 包。有关更多详细信息,请参见 In Golang, why are iterations over maps random?

1
谢谢您发布这个,现在更加清晰易懂了。 - Mark
一直在尝试这个,想分享一个多维示例,它很简单但可能对某些人有帮助。如果有更好的方法,请告诉我,我是 Go 的新手。https://play.golang.org/p/jggEnXxTYvN - Rens Tillmann

31

Go的映射(maps)并不维护插入顺序;您将需要自行实现此行为。

示例:

type NavigationMap struct {
    m map[string]navbarTab
    keys []string
}

func NewNavigationMap() *NavigationMap { ... }

func (n *NavigationMap) Set(k string, v navbarTab) {
    n.m[k] = v
    n.keys = append(n.keys, k)
}

这个示例并不完整,也没有涵盖所有用例(例如在重复键上更新插入顺序)。

如果您的用例包括多次重新插入相同的键(如果该键已经存在于地图中,则不会更新键 k 的插入顺序):

func (n *NavigationMap) Set(k string, v navbarTab) {
    _, present := n.m[k]
    n.m[k] = v
    if !present {
        n.keys = append(n.keys, k)
    }
}

选择最简单的东西来满足你的要求。


1
谢谢,我添加了一个切片来跟踪键,但我更喜欢你的版本。 - Mark
在我的看法中,如果键已经存在于映射表中,Set()函数应该只是追加到keys。也就是说,在第一行之前执行_, present := n.m[k],然后在追加的周围使用if!present { ... } - Ben Hoyt
@BenHoyt 我扩展了答案以明确并非所有用例都被覆盖。 - Arjan
一直在尝试这个,想分享一个多维度的例子,虽然很简单但可能会对某些人有所帮助。如果有更好的方法,请告诉我,我是Go的新手。https://play.golang.org/p/jggEnXxTYvN - Rens Tillmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接