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
	}
}
7 回复

非常感谢您的详细解释。

更多关于Golang中为什么这个循环的迭代次数不固定的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


为什么这个循环的迭代次数不固定?

迭代次数是固定的,但顺序不是。

[LE] 你遇到这个奇怪效果是因为这条指令:

m[key+"a"] = val

geosoft1:

这种奇怪的现象是由于这条指令造成的。

是的,我明白这一点。但我想理解为什么这里的迭代次数是不可预测的。根据我的理解,这应该是一个无限循环,因为在每次迭代中我都在向映射添加一个新条目,所以每次迭代后我们都会有一个新的映射条目可供迭代。

关于使用 range 进行迭代,语言规范中说明:

映射的迭代顺序是未指定的,并且不保证在每次迭代中都相同。如果在迭代过程中删除了一个尚未被访问到的映射条目,则不会生成对应的迭代值。如果在迭代过程中创建了一个映射条目,该条目可能在本次迭代中被生成,也可能被跳过。对于每个创建的条目以及每次迭代,选择可能各不相同。如果映射为 nil,则迭代次数为 0。

感谢您提供的语言规范参考。

NobbZ: 如果在迭代过程中删除了一个尚未到达的映射条目,则不会生成相应的迭代值。如果在迭代过程中创建了一个映射条目,该条目可能在迭代过程中被生成,也可能被跳过。

那么这意味着如果我们在映射迭代期间放入一个新条目,并不能保证会在映射中创建一个新条目。我的理解正确吗?

此外,我想了解为什么在 Golang 中会有这种实现方式?

在 Java 中,如果我们在映射迭代期间尝试向映射中放入元素,会得到 ConcurrentModificationException。我的理解是:既然 Golang 在这种情况下不会引发恐慌,那么它应该会导致无限循环。

您能否更详细地向我解释一下,以帮助我理解这个概念?

谢谢。

Prithvipal_Singh:

那么,这是否意味着在映射迭代过程中放入一个新条目时,并不能保证新条目会被创建在映射中?我的理解正确吗?

不,这是不正确的。

如果你在迭代同一个映射时放入一个新条目,它会被添加进去,尽管不能保证这个新元素会被迭代到。

你可以在循环后检查你的映射,你所有的元素,甚至那些未被迭代到的,都会在那里。

你可以假设 Go 在迭代映射时大致会执行以下操作,至少在语义上是这样的:

  1. 获取一个键的列表
  2. 随机化其顺序
  3. 在迭代时,保持一个索引来记录我们当前在这个列表中的位置

然后,当你向映射中添加一个条目时,它也会在键列表中创建一个对应的条目,但是,它会将该条目放在列表中的一个随机位置。如果条目被添加到了当前位置的左侧(为了避免重复迭代同一个键),它会将当前位置索引向前移动;如果插入到了更高的位置,则保持索引不变。

如果新键被插入到更高的位置,我们将在迭代过程中遇到它;如果它被添加到了更低的位置,我们将不会看到它。

在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,从而获得确定性的迭代行为。

回到顶部