# 前言

在Go语言中,遍历是日常开发中最常见的操作之一。不同的遍历方式会对性能产生显著影响。本文将深入分析:

  1. 基本切片遍历的性能特点
  2. 结构体切片的遍历优化
  3. 指针切片的性能优势
  4. 实际场景中的最佳实践

# 三种遍历方式对比

Go语言中主要有三种遍历切片的方式:

// 1. 索引遍历
for i := 0; i < len(slice); i++ {
    // 使用slice[i]
}

// 2. range遍历值
for _, v := range slice {
    // 使用v
}

// 3. range遍历索引和值
for i, v := range slice {
    // 使用i和v
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# []int Benchmark测试

func BenchmarkIndexLoop(b *testing.B) {
    slice := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        for j := 0; j < len(slice); j++ {
            _ = slice[j]
        }
    }
}

func BenchmarkRangeValue(b *testing.B) {
    slice := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        for _, v := range slice {
            _ = v
        }
    }
}

func BenchmarkRangeIndexValue(b *testing.B) {
    slice := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        for j, v := range slice {
            _, _ = j, v
        }
    }
}

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

测试结果如下,可以发现三者性能接近,基本相差不大。

% go test -bench=. -benchmem .
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkIndexLoop-12            4721586               235.7 ns/op             0 B/op          0 allocs/op
BenchmarkRangeValue-12           5085130               234.3 ns/op             0 B/op          0 allocs/op
BenchmarkRangeIndexValue-12      5101604               233.9 ns/op             0 B/op          0 allocs/op
PASS
ok      main/demo       4.560s

1
2
3
4
5
6
7
8
9
10

# []struct Benchmark 测试


type Item struct {
	ID   int
	Data [4096]byte // 增加数据量以放大性能差异
}

func BenchmarkStructIndex(b *testing.B) {
	var slice [1000]Item
	for i := 0; i < b.N; i++ {
		var tmp int
		for j := 0; j < len(slice); j++ {
			tmp = slice[j].ID
		}
		_ = tmp
	}
}

func BenchmarkStructRangeValue(b *testing.B) {
	var slice [1000]Item
	for i := 0; i < b.N; i++ {
		var tmp int
		for _, v := range slice {
			tmp = v.ID
		}
		_ = tmp
	}
}

func BenchmarkStructRangeIndexValue(b *testing.B) {
	var slice [1000]Item
	for i := 0; i < b.N; i++ {
		var tmp int
		for j := range slice {
			tmp = slice[j].ID
		}
		_ = tmp
	}
}

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

运行结果如下,可以发现通过索引的方式取值的两种方式相差不大,但是直接取值的方式性能差了 500 多倍,这是因为直接取值时会进行数据的复制,而索引取值不会进行数据的复制。

$ go test  -bench=. -benchmem . 
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStructIndex-12                  4735326               237.9 ns/op             0 B/op          0 allocs/op
BenchmarkStructRangeValue-12               17096             69135 ns/op               0 B/op          0 allocs/op
BenchmarkStructRangeIndexValue-12        5123646               234.6 ns/op             0 B/op          0 allocs/op
PASS
ok      main/demo       4.983s

1
2
3
4
5
6
7
8
9
10

# []*struch Benchmark 测试


type Item struct {
	ID   int
	Data [4096]byte // 增加数据量以放大性能差异
}

func BenchmarkStructIndex(b *testing.B) {
	var slice [1000]*Item
	for i := range slice {
		slice[i] = &Item{}
	}
	for i := 0; i < b.N; i++ {
		var tmp int
		for j := 0; j < len(slice); j++ {
			tmp = slice[j].ID
		}
		_ = tmp
	}
}

func BenchmarkStructRangeValue(b *testing.B) {
	var slice [1000]*Item
	for i := range slice {
		slice[i] = &Item{}
	}
	for i := 0; i < b.N; i++ {
		var tmp int
		for _, v := range slice {
			tmp = v.ID
		}
		_ = tmp
	}
}

func BenchmarkStructRangeIndexValue(b *testing.B) {
	var slice [1000]*Item
	for i := range slice {
		slice[i] = &Item{}
	}
	for i := 0; i < b.N; i++ {
		var tmp int
		for j := range slice {
			tmp = slice[j].ID
		}
		_ = tmp
	}
}

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

运行结果如下,三者性能相近,但是有个好处是可以直接修改指针对应结构体的值。

go test  -bench=. -benchmem .
goos: darwin
goarch: arm64
pkg: main/demo
cpu: Apple M4 Pro
BenchmarkStructIndex-12                  1864108               656.9 ns/op             2 B/op          0 allocs/op
BenchmarkStructRangeValue-12             1577792               748.6 ns/op             3 B/op          0 allocs/op
BenchmarkStructRangeIndexValue-12        1843874               661.4 ns/op             2 B/op          0 allocs/op
PASS
ok      main/demo       5.995s

1
2
3
4
5
6
7
8
9
10

# 总结

根据测试结果,我们得出以下结论:

  1. 基础类型切片:三种遍历方式性能相当,可按编码习惯选择
  2. 大结构体切片
    • 避免使用for _, v := range直接值遍历
    • 优先使用索引遍历或range索引遍历
  3. 需要修改元素时
    • 使用指针切片([]*T)性能接近且更灵活
  4. 性能关键路径
    • 对大结构体集合操作,索引遍历性能最优
    • 对小结构体或基础类型,差异可忽略

记住:没有绝对的最佳方式,只有最适合当前场景的选择!