Golang中为什么这个循环的迭代次数不固定
Golang中为什么这个循环的迭代次数不固定 为什么这个循环的迭代次数不固定?每次执行这段代码,我都会观察到不同的迭代次数。
package main
import "fmt"
func main() {
m := make(map[string]string)
m["A"] = "Apple"
for key, val := range m {
fmt.Printf("key=%v, val=%v\n", key, val)
m[key+"a"] = val
}
}
geosoft1:
这种奇怪的现象是由于这条指令造成的。
是的,我明白这一点。但我想理解为什么这里的迭代次数是不可预测的。根据我的理解,这应该是一个无限循环,因为在每次迭代中我都在向映射添加一个新条目,所以每次迭代后我们都会有一个新的映射条目可供迭代。
关于使用 range 进行迭代,语言规范中说明:
映射的迭代顺序是未指定的,并且不保证在每次迭代中都相同。如果在迭代过程中删除了一个尚未被访问到的映射条目,则不会生成对应的迭代值。如果在迭代过程中创建了一个映射条目,该条目可能在本次迭代中被生成,也可能被跳过。对于每个创建的条目以及每次迭代,选择可能各不相同。如果映射为 nil,则迭代次数为 0。
感谢您提供的语言规范参考。
NobbZ: 如果在迭代过程中删除了一个尚未到达的映射条目,则不会生成相应的迭代值。如果在迭代过程中创建了一个映射条目,该条目可能在迭代过程中被生成,也可能被跳过。
那么这意味着如果我们在映射迭代期间放入一个新条目,并不能保证会在映射中创建一个新条目。我的理解正确吗?
此外,我想了解为什么在 Golang 中会有这种实现方式?
在 Java 中,如果我们在映射迭代期间尝试向映射中放入元素,会得到 ConcurrentModificationException。我的理解是:既然 Golang 在这种情况下不会引发恐慌,那么它应该会导致无限循环。
您能否更详细地向我解释一下,以帮助我理解这个概念?
谢谢。
Prithvipal_Singh:
那么,这是否意味着在映射迭代过程中放入一个新条目时,并不能保证新条目会被创建在映射中?我的理解正确吗?
不,这是不正确的。
如果你在迭代同一个映射时放入一个新条目,它会被添加进去,尽管不能保证这个新元素会被迭代到。
你可以在循环后检查你的映射,你所有的元素,甚至那些未被迭代到的,都会在那里。
你可以假设 Go 在迭代映射时大致会执行以下操作,至少在语义上是这样的:
- 获取一个键的列表
- 随机化其顺序
- 在迭代时,保持一个索引来记录我们当前在这个列表中的位置
然后,当你向映射中添加一个条目时,它也会在键列表中创建一个对应的条目,但是,它会将该条目放在列表中的一个随机位置。如果条目被添加到了当前位置的左侧(为了避免重复迭代同一个键),它会将当前位置索引向前移动;如果插入到了更高的位置,则保持索引不变。
如果新键被插入到更高的位置,我们将在迭代过程中遇到它;如果它被添加到了更低的位置,我们将不会看到它。
在Go语言中,使用range迭代map时,迭代顺序是不确定的,并且如果在迭代过程中修改map(添加或删除元素),行为是未定义的。这可能导致迭代次数不固定,甚至引发运行时panic。
在你的代码中,每次迭代都向mapm中添加一个新键值对,这改变了map的结构。由于map的迭代顺序不确定,并且迭代过程中修改map会导致未定义行为,因此每次运行可能产生不同的结果。
示例代码演示了这种行为:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Printf("Run %d:\n", i+1)
m := make(map[string]string)
m["A"] = "Apple"
count := 0
for key, val := range m {
count++
fmt.Printf(" Iteration %d: key=%v, val=%v\n", count, key, val)
m[key+"a"] = val
}
fmt.Printf("Total iterations: %d\n\n", count)
}
}
运行这个示例,你可能会观察到不同的迭代次数。这是因为在迭代过程中修改map破坏了迭代器的内部状态,导致不可预测的行为。
要避免这个问题,应该在迭代前复制需要修改的键,或者使用单独的slice来记录要添加的键值对:
package main
import "fmt"
func main() {
m := make(map[string]string)
m["A"] = "Apple"
// 方法1: 先收集要添加的键
var toAdd []string
for key, val := range m {
fmt.Printf("key=%v, val=%v\n", key, val)
toAdd = append(toAdd, key+"a")
}
// 迭代结束后再添加新键
for _, key := range toAdd {
m[key] = "Apple"
}
fmt.Println("Final map:", m)
}
这样保证了在迭代过程中不修改map,从而获得确定性的迭代行为。


