抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

前言

在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语言框架