传统的for和for range是golang唯二提供的能够遍历的循环结构,但是for range并不是简单的for语法糖,实际上他们之间仍然有很大的不同
var a = [3]int{1, 2, 3}
var r [3]int
for i, v := range a {
if i == 0 {
a[1] = 4
}
r[i] = v
// 1,2,3
t.Log(r[i])
}
// 1,2,3
t.Log(r)
// 1,4,3
t.Log(a)
以上代码输出的r并非期望的[1,4,3],而是一个[1,2,3]。那么这是不是意味着没有更新成功呢?
再来看另一段代码
a := []int{1, 2, 3}
for i, v := range a {
if i == 0 {
a[1] = 4
}
// 输出1,4,3
t.Log(v)
}
// 1,4,3
t.Log(a)
与前段代码不同的在于数组a在遍历过程中的值发生了改变
那么这是怎么回事呢?
首先,让我们看看两段代码的不同之处,除了第一段多用一个数组进行储存遍历过程中的元素之外,还有就是第一段是数组而非slice,让我们改成slice试试看
var a = []int{1, 2, 3}
r := make([]int, 3)
for i, v := range a {
if i == 0 {
a[1] = 4
}
r[i] = v
// 1, 4, 3
t.Log(r[i])
}
// 1, 4, 3
t.Log(r)
// 1, 4, 3
t.Log(a)
结果居然和第二段代码一致,无论是打印的,还是r、a都是1、4、3,也就是值确实是发生了更新
显然我们可以得出一个结论——那就是for range对于数组和slice更新的结果并不完全一致。那这是for range针对两种不同的数据结构作出不同的处理了呢?
或许可以回到slice和数组的区别上来。我们知道数组是固定的,最基本的结构,而slice是动态的,由一个slice header表示。
而for range在go tour中就说的很清楚了,是元素的复制,那么对于数组来说,数组复制就是复制全部,而slice复制仅仅只会slice header结构,那么对slice的遍历更改还是会直接影响到slice,但是array却并不会,只会在遍历结束后,更新到原数组
那么基于此,一个猜想提出了,会不会for range这种复制的形势会更加消耗资源呢?
以下测试在macOS下,cpu为Intel® Core™ i5-1038NG7 CPU @ 2.00GHz,数据量为100 000,go版本为1.18
for | for range | |
---|---|---|
int | 31830 | 36660 |
1 int结构体 | 34304 | 38568 |
2 int结构体 | 35278 | 183267 |
3 int结构体 | 35219 | 200697 |
4 int结构体 | 34232 | 322685 |
5 int结构体 | 34219 | 620547 |
从表中可以看出在int情况下for range相比于for性能慢15%
而对于结构体,差的就更远了,在5 int结构体情况下,for range相比于for性能慢1700%,这是相当大的差距,显然这提醒我们在结构体复杂的大数据量遍历还是尽量不用for range
不出意料的是for range确实比普通的for性能消耗更多,但是值得注意的是在结构体的情况下,for range的性能消耗以一个惊人的速度上升,这是怎么回事呢?
从https://github.com/golang/go/issues/24416来看,似乎是因为大结构体无法被ssa优化,并且这个优化在go1.18看起来仍未完成
SSA(Single-assignment form)是一种中间表示形式属性,其要求每个变量只被赋值一次,并在使用之前被定义。