迭代 map 是误解和错误的常见来源,主要是因为开发人员做出了错误的假设。在这个例子中,我们将讨论两种不同的情况:
- 排序
- 迭代期间的 map 更新
本节将看到在迭代 map 时基于错误假设的两个常见错误。
关于排序,我们应该了解 map 数据结构的一些基本行为:
- 它不会按键对数据进行排序( map 不是基于二叉树)。
- 它不保留添加数据的顺序。例如,如果我们在对 B 之前插入对 A,我们不应该基于此插入顺序做出任何假设。
此外,在 map 上迭代时,我们根本不应该做出任何排序假设。让我们了解一下这句话的含义。
我们将考虑以下由四个桶组成的映射(元素代表键):
后备数组点的每个索引都引用给定的存储桶。现在,让我们使用 range
循环遍历此映射并打印所有键:
for k := range m {
fmt.Print(k)
}
我们提到键不是按键排序的。因此,我们不能指望这段代码打印 acdeyz
。
同时,我们说 map 不保留插入顺序。因此,我们也不能指望这段代码打印 ayzcde
。
然而,我们至少可以期望这段代码按照它们当前存储在 map 中的顺序打印键吗,aczdey
?不,连这个都没有。在 Go 中,未指定 map 上的迭代顺序。此外,不能保证从一次 迭代到下一次迭代的顺序相同。我们应该牢记这些 map 的行为,以免我们的代码基于错误的假设。
我们可以通过运行前面的循环两次来确认所有这些语句:
zdyaec
czyade
正如我们所注意到的,从一个迭代到另一个迭代的顺序是不同的。
Note 虽然不能保证迭代顺序,但迭代分布并不均匀。这就是为什么官方 Go 规范声明迭代是未指定的,而不是随机的。
那么为什么 Go 有这样一种令人惊讶的方式来遍历 map 呢?这是语言设计者有意识的选择。他们希望添加某种形式的随机性,以确保开发人员在使用 map 时永远不会依赖任何排序假设。
因此,作为 Go 开发人员,我们不应该在迭代 map 时对排序做出假设。但是,请注意,使用标准库或外部包中的包可能会导致不同的行为。例如,当 encoding/json
包将映射编组为 JSON 时,它将按键的字母顺序对数据进行重新排序,而不管插入顺序如何。然而,这不是 Go map 本身的属性。如果需要排序,我们应该依赖其他数据结构,例如二进制 堆(GoDS库包含有用的数据结构实现)。
现在让我们来看第二个错误,它与在迭代 map 时更新 map 有关。
在 Go 中,允许在迭代期间更新 map (插入或删除元素);它不会导致编译或运行时错误。但是,在迭代期间在映射中添加条目时,我们应该考虑另一个方面。否则,可能会导致不确定的结果。
让我们检查以下在 map[int]bool
上迭代的示例。 如果对值是 true,我们将添加另一个元素。你能猜出这段代码的输出是什么吗?
m := map[int]bool{
0: true,
1: false,
2: true,
}
for k, v := range m {
if v {
m[10+k] = true
}
}
fmt.Println(m)
这段代码的结果是不可预测的。例如,如果我们多次运行此代码:
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true]
map[0:true 1:false 2:true 10:true 12:true 20:true]
为了理解原因,我们必须阅读 Go 规范在迭代期间出现新映射条目时的说明:
如果在迭代期间创建了映射条目,则它可能在迭代期间产生或被跳过。对于创建的每个条目以及从一个迭代到下一个迭代,选择可能会有所不同。-- Go 规范
因此,在迭代期间将元素添加到 map 时,它可能会在后续迭代期间产生,也可能不会。作为 Go 开发人员,我们没有任何方法来强制执行该行为。此外,它可能会因一次迭代而异,这就是为什么我们得到了三次不同的结果。
必须牢记这种行为,以确保我们的代码不会产生不可预测的输出。如果我们想在迭代时更新 map 并确保添加的条目不是迭代的一部分,一种解决方案是像这样处理 map 的副本:
m := map[int]bool{
0: true,
1: false,
2: true,
}
m2 := copyMap(m)
for k, v := range m {
m2[k] = v
if v {
m2[10+k] = true
}
}
fmt.Println(m2)
在此示例中,我们将正在读取的 map 与正在更新的 map 分离。事实上,我们一直在迭代 m
,但更新是在 m2
上完成的。这个新版本创建了一个可预测和可重复的输出:
map[0:true 1:false 2:true 10:true 12:true]
总而言之,当我们使用 map 时,我们不应该依赖:
- 通过键对数据进行排序
- 插入顺序的保存
- 确定性迭代顺序
- 在迭代期间添加的元素将在此迭代期间产生的事实
牢记这些行为应该有助于我们避免基于错误假设的常见错误。
在下一节中,我们将看到在打破循环时经常犯的错误。