一些关于遍历映射条目的细节:
- 映射中的条目的遍历顺序是不确定的(可以认为是随机的)。或者说,同一个映射中的条目的两次遍历中,条目的顺序很可能是不一致的,即使在这两次遍历之间,此映射并未发生任何改变。
- 如果在一个映射中的条目的遍历过程中,一个还没有被遍历到的条目被删除了,则此条目保证不会被遍历出来。
- 如果在一个映射中的条目的遍历过程中,一个新的条目被添加入此映射,则此条目并不保证将在此遍历过程中被遍历出来。
如果可以确保没有其它协程操纵一个映射m
,则下面的代码保证将清空m
中所有条目。
for key := range m { delete(m, key) }
当然,数组和切片元素也可以用传统的for
循环来遍历。
for i := 0; i < len(anArrayOrSlice); i++ { element := anArrayOrSlice[i] // ... }
对一个for-range
循环代码块
for key, element = range aContainer {...}
有三个重要的事实存在:
- 被遍历的容器值是
aContainer
的一个副本。 注意,只有aContainer
的直接部分被复制了。 此副本是一个匿名的值,所以它是不可被修改的。- 如果
aContainer
是一个数组,那么在遍历过程中对此数组元素的修改不会体现到循环变量中。 原因是此数组的副本(被真正遍历的容器)和此数组不共享任何元素。 - 如果
aContainer
是一个切片(或者映射),那么在遍历过程中对此切片(或者映射)元素的修改将体现到循环变量中。 原因是此切片(或者映射)的副本和此切片(或者映射)共享元素(或条目)。
- 如果
- 在遍历中的每个循环步,
aContainer
副本中的一个键值元素对将被赋值(复制)给循环变量。 所以对循环变量的直接部分的修改将不会体现在aContainer
中的对应元素中。 (因为这个原因,并且for-range
循环是遍历映射条目的唯一途径,所以最好不要使用大尺寸的映射键值和元素类型,以避免较大的复制负担。) - 所有被遍历的键值对将被赋值给同一对循环变量实例。
下面这个例子验证了上述第一个和第二个事实
package main import "fmt" func main() { type Person struct { name string age int } persons := [2]Person {{"Alice", 28}, {"Bob", 25}} for i, p := range persons { fmt.Println(i, p) // 此修改将不会体现在这个遍历过程中, // 因为被遍历的数组是persons的一个副本。 persons[1].name = "Jack" // 此修改不会反映到persons数组中,因为p // 是persons数组的副本中的一个元素的副本。 p.age = 31 } fmt.Println("persons:", &persons) }
输出结果:
0 {Alice 28} 1 {Bob 25} persons: &[{Alice 28} {Jack 25}]
如果我们将上例中的数组改为一个切片,则在循环中对此切片的修改将在循环过程中体现出来。 但是对循环变量的修改仍然不会体现在此切片中。
... // 改为一个切片。 persons := []Person {{"Alice", 28}, {"Bob", 25}} for i, p := range persons { fmt.Println(i, p) // 这次,此修改将反映在此次遍历过程中。 persons[1].name = "Jack" // 这个修改仍然不会体现在persons切片容器中。 p.age = 31 } fmt.Println("persons:", &persons) }
输出结果变成了:
0 {Alice 28} 1 {Jack 25} persons: &[{Alice 28} {Jack 25}]
下面这个例子验证了上述的第二个和第三个事实:
package main import "fmt" func main() { langs := map[struct{ dynamic, strong bool }]map[string]int{ {true, false}: {"JavaScript": 1995}, {false, true}: {"Go": 2009}, {false, false}: {"C": 1972}, } // 此映射的键值和元素类型均为指针类型。 // 这有些不寻常,只是为了讲解目的。 m0 := map[*struct{ dynamic, strong bool }]*map[string]int{} for category, langInfo := range langs { m0[&category] = &langInfo // 下面这行修改对映射langs没有任何影响。 category.dynamic, category.strong = true, true } for category, langInfo := range langs { fmt.Println(category, langInfo) } m1 := map[struct{ dynamic, strong bool }]map[string]int{} for category, langInfo := range m0 { m1[*category] = *langInfo } // 映射m0和m1中均只有一个条目。 fmt.Println(len(m0), len(m1)) // 1 1 fmt.Println(m1) // map[{true true}:map[C:1972]] }
上面已经提到了,映射条目的遍历顺序是随机的。所以下面前三行的输出顺序可能会略有不同:
{false true} map[Go:2009] {false false} map[C:1972] {true false} map[JavaScript:1995] 1 1 map[{true true}:map[Go:2009]]
复制一个切片或者映射的代价很小,但是复制一个大尺寸的数组的代价比较大。 所以,一般来说,range
关键字后跟随一个大尺寸数组不是一个好主意。 如果我们要遍历一个大尺寸数组中的元素,我们以遍历从此数组派生出来的一个切片,或者遍历一个指向此数组的指针(详见下一节)。
对于一个数组或者切片,如果它的元素类型的尺寸较大,则一般来说,用第二个循环变量来存储每个循环步中被遍历的元素不是一个好主意。 对于这样的数组或者切片,我们最好忽略或者舍弃for-range
代码块中的第二个循环变量,或者使用传统的for
循环来遍历元素。 比如,在下面这个例子中,函数fa
中的循环效率比函数fb
中的循环低得多。
type Buffer struct { start, end int data [1024]byte } func fa(buffers []Buffer) int { numUnreads := 0 for _, buf := range buffers { numUnreads += buf.end - buf.start } return numUnreads } func fb(buffers []Buffer) int { numUnreads := 0 for i := range buffers { numUnreads += buffers[i].end - buffers[i].start } return numUnreads }
把数组指针当做数组来使用
对于某些情形,我们可以把数组指针当做数组来使用。
我们可以通过在range
关键字后跟随一个数组的指针来遍历此数组中的元素。 对于大尺寸的数组,这种方法比较高效,因为复制一个指针比复制一个大尺寸数组的代价低得多。 下面的例子中的两个循环是等价的,它们的效率也基本相同。package main import "fmt" func main() { var a [100]int for i, n := range &a { // 复制一个指针的开销很小 fmt.Println(i, n) } for i, n := range a[:] { // 复制一个切片的开销很小 fmt.Println(i, n) } }
如果一个for-range
循环中的第二个循环变量既没有被忽略,也没有被舍弃,并且range
关键字后跟随一个nil数组指针,则此循环将造成一个恐慌。 在下面这个例子中,前两个循环都将打印出5个下标,但最后一个循环将导致一个恐慌。
package main import "fmt" func main() { var p *[5]int // nil for i, _ := range p { // okay fmt.Println(i) } for i := range p { // okay fmt.Println(i) } for i, n := range p { // panic fmt.Println(i, n) } }