抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

两种遍历方式的使用与区别

for 和 range 区别

range 可以用来遍历 “引用六兄弟” 中的三个:slice、map、chan,加上数组 array。

对于数组和 slice,还可以用 for 来遍历。

基本使用

slice/array

  • 对于 range 循环,循环次数会在循环开始前计算好,在循环过程中修改切片长度并不会改变本次循环的次数。
  • 对于空切片,循环次数为 0。
1
2
3
4
5
6
7
8
9
10
11
s := []int{11, 99, 16, 55}

for i, v := range s {
fmt.Println(i, v)
}

// Output:
0 11
1 99
2 16
3 55

这种方式中,v 是 s 中元素的一份拷贝,对 v 的修改并不会影响 s 中原本的元素。

1
2
3
4
5
6
7
8
9
10
s := []int{11, 99, 16, 55}

for i, v := range s {
v += 10 // v 只是一份拷贝,并不会影响 s 里的元素
}

fmt.Println(s)

// Output:
[11 99 16 55]

除非 s 中的元素是指针,则对 v 的修改会影响 s,下面将会讲到。

此外采用下面两种方式也可以修改 s 中的元素。

1
2
3
4
5
6
7
8
9
10
s := []int{11, 99, 16, 55}

for i := 0; i < len(s); i++ {
s[i] += 10
}

fmt.Println(s)

// Output:
[21 109 26 65]
1
2
3
4
5
6
7
8
9
10
s := []int{11, 99, 16, 55}

for i := range s {
s[i] += 10
}

fmt.Println(s)

// Output:
[21 109 26 65]

map

  • 迭代过程中,删除还没遍历到的键值对,则该键值对不会被遍历。

  • 迭代过程中,如果创建新的键值对,则新的键值对有可能被遍历,也有可能不会被遍历。

  • 对于空字典,遍历次数为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
m := map[byte]int{
411: 10,
422: 20,
433: 30,
444: 40,
}

for k, v := range m {
4v += 5 // 仅仅是对拷贝的修改,不会影响 m
4fmt.Println(k, v)
}

fmt.Println(m)

// Output:
22 25
33 35
44 45
11 15
map[11:10 22:20 33:30 44:40]

同样这种方式对 v 进行修改是不会影响 m 的。

下面这种就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
m := map[byte]int{
411: 10,
422: 20,
433: 30,
444: 40,
}

for k := range m {
4m[k] += 1 // 这种方式就会修改到 m 的元素
4fmt.Println(k, m[k])
}

fmt.Println(m)

// Output:
11 11
22 21
33 31
44 41
map[11:11 22:21 33:31 44:41]

chan

  • 发送给信道的值,可以通过 range 遍历,直到信道被关闭,才会退出循环。

  • 如果是未分配内存的 nil 信道,循环将永远阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ch := make(chan byte)

go func() {
ch <- 'b'
ch <- 'o'
ch <- 'i'
ch <- 'i'
close(ch)
}()

for c := range ch {
fmt.Println(string(c))
}

// Output:
b
o
i
i

性能比较

接下来我们主要针对基本类型和结构体类型,比较他们使用 for 和 range 的性能。

只有 slice 和 array 才有办法 for 和 range 都使用,所以接下来的比对如下:

for range
基本类型 for + []int range + []int
结构体 for + []struct range + []struct

for + []int VS range + []int

主要方式有 3 种:

  • for i:=0; i < len(s); i++
  • for i := range s
  • for i, v := range s

先说结论:

遍历 []int ,三种方式并没太大差别。

[]int

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
47
48
49
50
51
52
// 生成切片
func geneI(n int) []int {
4nums := make([]int, 0, n)
4rand.Seed(time.Now().Unix())
4for z := 0; z < n; z++ {
44nums = append(nums, rand.Int())
4}
4return nums
}

// for 遍历
func I_For(nums []int) {
4n := len(nums)
4for i := 0; i < n; i++ {
44_ = nums[i]
4}
}

// range i 遍历
func I_RangeI(nums []int) {
4for i := range nums {
44_ = nums[i]
4}
}

// range iv 遍历
func I_RangeIV(nums []int) {
4for i, v := range nums {
44_, _ = i, v
4}
}

func BenchmarkI_For(b *testing.B) {
4nums := geneI(1 << 20)
4for i := 0; i < b.N; i++ {
44I_For(nums)
4}
}

func BenchmarkI_RangeI(b *testing.B) {
4nums := geneI(1 << 20)
4for i := 0; i < b.N; i++ {
44I_RangeI(nums)
4}
}

func BenchmarkI_RangeIV(b *testing.B) {
4nums := geneI(1 << 20)
4for i := 0; i < b.N; i++ {
44I_RangeIV(nums)
4}
}

执行后的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ go version 
go1.16.6 linux/amd64

$ go test . -bench=I_ -benchmem
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkI_For-4 2685 431122 ns/op 3124 B/op 0 allocs/op
BenchmarkI_RangeI-4 2686 431759 ns/op 3123 B/op 0 allocs/op
BenchmarkI_RangeIV-4 2688 432650 ns/op 3120 B/op 0 allocs/op
PASS
ok 3.586s

从结果可以看出,在遍历基本类型切片时,不管是时间上(ns/op),还是空间上(B/op),还是系统调用上(allocs/op),都没啥差别。

现在是 2021-8-19,以上是 go 1.16.6 版本测试的结果,并没有太大差别,不像网上说的 for 比 range 快。

[]struct

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
47
48
49
50
51
52
53
54
55
const N = 1 << 20
type Per struct {
4ins [2048]byte
4age int
}

func geneS(n int) []Per {
4persons := make([]Per, 0, n)
4rand.Seed(time.Now().Unix())
4for i := 0; i < n; i++ {
44persons = append(persons, Per{age: rand.Int(), ins: [2048]byte{'a'}})
4}
4return persons
}

func S_For(persons []Per) {
4n := len(persons)
4for i := 0; i < n; i++ {
44_ = persons[i].age
4}
}

func S_RangeI(persons []Per) {
4for i := range persons {
44_ = persons[i].age
4}
}

func S_RangeIV(persons []Per) {
4for i, v := range persons {
44_, _ = i, v.age
4}
}

func BenchmarkS_For(b *testing.B) {
4persons := geneS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44S_For(persons)
4}
}
func BenchmarkS_RangeI(b *testing.B) {
4persons := geneS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44S_RangeI(persons)
4}
}
func BenchmarkS_RangeIV(b *testing.B) {
4persons := geneS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44S_RangeIV(persons)
4}
}

运行结果

1
2
3
4
5
6
7
8
9
$ go test . -bench=S_ -benchmem
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkS_For-4 2850 421009 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeI-4 2850 420993 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeIV-4 2848 421131 ns/op 0 B/op 0 allocs/op
PASS
ok 7.330s

可以看到基本也没太大区别。

[]*struct

结论:即使是结构体指针切片,跑出来的结果几乎相差无几

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
func PS_For(persons []*Per) {
4n := len(persons)
4for i := 0; i < n; i++ {
44_ = persons[i].age
4}
}

func PS_RangeI(persons []*Per) {
4for i := range persons {
44_ = persons[i].age
4}
}

func PS_RangeIV(persons []*Per) {
4for i, v := range persons {
44_, _ = i, v.age
4}
}

func BenchmarkPS_For(b *testing.B) {
4persons := genePS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44PS_For(persons)
4}
}

func BenchmarkPS_RangeI(b *testing.B) {
4persons := genePS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44PS_RangeI(persons)
4}
}
func BenchmarkPS_RangeIV(b *testing.B) {
4persons := genePS(N)
4b.ResetTimer()
4for i := 0; i < b.N; i++ {
44PS_RangeIV(persons)
4}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
$ go test . -bench=S_ -benchmem  
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkS_For-4 83856697 14.13 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeI-4 66149228 17.66 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeIV-4 72271676 16.61 ns/op 0 B/op 0 allocs/op
BenchmarkPS_For-4 71399964 21.43 ns/op 0 B/op 0 allocs/op
BenchmarkPS_RangeI-4 63293829 18.62 ns/op 0 B/op 0 allocs/op
BenchmarkPS_RangeIV-4 63576286 18.57 ns/op 0 B/op 0 allocs/op
PASS
ok 7.565s

能修改元素的遍历

前面说 for i, v := range s 这种方式,v 是一份拷贝,对 v 的修改不会影响 s 中的元素。

但是如果 v 是个指针,对 v 的修改还是会影响的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a := 10
b := 20
c := 30

s := []*int{&a, &b, &c}

fmt.Println(a, b, c)
for _, v := range s {
*v += 1
}
fmt.Println(a, b, c)

// Output:
10 20 30
11 21 31

再举个结构体的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
name string
age int
}

persons := make([]*Person, 0, 3)
persons = append(persons, &Person{age: 15}, &Person{age: 25}, &Person{age: 35})

fmt.Println(persons[0], persons[1], persons[2])
for _, v := range persons {
v.age += 1
}
fmt.Println(persons[0], persons[1], persons[2])

// Output:
&{ 15} &{ 25} &{ 35}
&{ 16} &{ 26} &{ 36}

总结

遍历总共有 3 种方式:

  1. forfor i:=0; i<n; i++
  2. rangeIfor i := range items
  3. rangeIVfor i, v := range items

三种方式都各有所长。其中 rangeIV 中 v 是 items 中元素的一份拷贝,如果 v 不是指针,那么对于 v 的修改并不会影响 items 里面的元素,如果 v 是指针,则能够影响。

正是因为 v 是一份拷贝,所以在一些旧版本中可能会因此降低了性能,但在新版本(至少 go 1.16.6 ,我并没有去求证)中,从基准测试来看是没有什么区别,可能在新版本中做了优化。

哔哔