Go小细节:for-range

621 阅读3分钟

前言

在Go语言中提供了以下形式来遍历容器类型(array、slice、map),同时可以通过空标识符_忽略掉key或者value的赋值。下面来了解一下这四种循环语句需要注意的地方吧。

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则是共享相关的底层元素,例如下面的代码例子。

// 测试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数组,因为他们已经没有关系啦。

// 测试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方式来遍历一个大数组,再通过基准测试查看他们的执行时间

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