前言 在Go语言中提供了以下形式来遍历容器类型(array、slice、map),同时可以通过空标识符_
忽略掉key或者value的赋值。下面来了解一下这四种循环语句需要注意的地方吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for key, value = range container { // key:下标或者键 // value:值 } for _, value = range container { } for key,_ = range container { } for i := 0; i < len(arrayOrSlice); i++ { // 只适用于array和slice类型 }
细节 在使用for-range
语句遍历容器类型时有一些细节需要注意,在for-range中可能会存在多次值复制的成本。
容器副本 使用for-range中被遍历的容器值是其实一个副本,它对容器类型的直接部分进行拷贝,对于基本类型就是直接复制其值,对于引用部分就是复制它的地址。这就是意味着数组和数组副本之间是互不影响的,而对slice、map则是共享相关的底层元素 ,例如下面的代码例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 测试array遍历 func TestForRangeArray(t *testing.T) { type User struct { name string age int } users := [2]User{{"Tom", 18}, {"Jerry", 18}} for i, user := range users { users[1].age = 9 user.name = "Modify-" + user.name fmt.Println(i, "for-range", user) } fmt.Println(users) } // 0 for-range {Modify-Tom 18} // 1 for-range {Modify-Jerry 18} // [{Tom 18} {Jerry 9}]
通过输出语句看到users[1].age = 9
这行代码并没有影响到user循环变量,同时user.name = "Modify-" + user.name
也没有影响到原本的users数组,因为他们已经没有关系啦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 测试slice遍历 func TestForRangeSlice(t *testing.T) { type User struct { name string age int } users := []User{{"Tom", 18}, {"Jerry", 18}} for i, user := range users { users[1].age = 9 user.name = "Modify-" + user.name fmt.Println(i, "for-range", user) } fmt.Println(users) } // 0 for-range {Modify-Tom 18} // 1 for-range {Modify-Jerry 9} // [{Tom 18} {Jerry 9}]
通过输出语句看到users[1].age = 9
已经改变了下一次的user循环变量的值。
容器值副本 在for-range遍历中的每个循环步中,容器副本中的键值元素都会被复制给循环变量,所以对容器副本本身的元素也没有关系了,正如上面的代码所示user.name = "Modify-" + user.name
这行代码对于array还是slice都没有影响到users,这也是因为user循环变量也是容器副本的一个副本。
for-range效率 正是因为在for-range语句中存在这些细节,使用不同的方式去循环容器类型,性能效率也是不一样的。
下面我们使用三种不同的for-range方式来遍历一个大数组,再通过基准测试查看他们的执行时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 type BigArray [1000000]int64 var bigArrayX BigArray var bigArrayY BigArray var bigArrayZ BigArray var sumX, sumY, sumZ int64 func BenchmarkStandard(b *testing.B) { for i := 0; i < b.N; i++ { sumX = 0 for j := 0; j < len(bigArrayX); j++ { sumX += bigArrayX[j] } } } func BenchmarkForRangeKey(b *testing.B) { for i := 0; i < b.N; i++ { sumY = 0 for j, _ := range bigArrayY { sumY += bigArrayY[j] } } } func BenchmarkForRangeValue(b *testing.B) { for i := 0; i < b.N; i++ { sumZ = 0 for _, v := range bigArrayZ { sumZ += v } } } // 执行结果 goos: windows goarch: amd64 pkg: study/test cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz BenchmarkStandard BenchmarkStandard-8 616 1664792 ns/op BenchmarkForRangeKey BenchmarkForRangeKey-8 741 1647255 ns/op BenchmarkForRangeValue BenchmarkForRangeValue-8 474 2543182 ns/op PASS
可以很清楚的看到BenchmarkForRangeValue
方法的for-range遍历方式相对于其他两个慢了许多,这是因为进行了多次的复制拷贝。因此如果对于大数组、大map类型推荐使用前面两种的for-range方式。
本文正在参加技术专题18期-聊聊Go语言框架