Go Range Loop Internals

Tagged as go, range, loop
Written on 2018-01-10 20:20:04

译文版权@归原文所有.

虽然他们非常方便, 但我总是发现 Go 的 Range 循环有点神秘. 我并不是第一个:

// http://bit.ly/2CXC1Ob 来自 Dave Cheney.
package main

func main() {
    v := []int{1, 2, 3}
    for i := range v {
        v = append(v, i)
    }
}

现在我可以把这些事实记录下来, 但是我很可能会忘记. 为了有更好的机会记住这个, 我需要找出为什么 range 循环会这样. 所以我写了这篇文章.

Step 1: 读手册(RTFM)

我们首先应该去读 range 循环文档. Go语言规范文档在 for 语句部分的 For 语句和 range 子句描述了 range 循环. 我不会在这里复制整个规范,我会总结一些有趣的部分.

首先, 让我们提醒自己我们在这里看到什么:

for i := range a {
    fmt.Println(i)
}

Range 变量

你们中的大多数人会知道, 在 Range 子句的左边(上面的例子中的 i), 你可以这样分配循环变量:

您也可以选择完全忽略循环变量.

如果使用短变量声明样式分配(:=), 则 Go 将在循环的每个迭代中重用变量(仅在循环内的范围内).

Range 表达式

在 Range 子句的右边(上面的例子中的 a), 你可以找到他们称之为 Range 表达式的东西. 它可以包含任何表达式, 其计算结果如下:

Range 表达式在开始循环之前只计算一次. 请注意, 这个规则有一个例外: 如果 Range 一个数组(或指向它的指针), 你只能分配索引:那么只有 len(a) 被计算. 仅计算 len(a) 意味着可以在编译时计算表达式 a, 并由编译器用常量替换. len 函数规范解释如下:

如果s的类型是数组或指向数组的指针并且表达式 s 不包含通道接收(channel receives) 或(非 常量) 函数调用, 则表达式 len(s) 和 cap(s) 是常量. 在这种情况下 s 不被计算. 否则, len 和 cap 的调用不是常量, 而是被计算.

那么 "计算(evaluated)" 究竟意味着什么呢? 不幸的是我不能在规范中找到这个信息. 当然, 我可以猜测, 这意味着完全执行表达式, 直到它不能进一步减少. 在任何情况下, 这里的高位是 Range 表达式在循环开始之前计算一次. 你如何只评估一个表达式仅一次? 通过将其分配给一个变量! 这可能是这里发生的事情吗?

有趣的是, 这个规范提到了一些关于从 map 中添加和删除的特殊的东西(没有提到切片):

如果在迭代过程中移除尚未到达的 map 项, 则不会生成相应的迭代值. 如果迭代过程中创建 map 项, 那么可能会在迭代过程中生成该项, 或者可能会跳过该项.

我稍后会回到 map.

Step 2: Range 支持的数据类型

如果我们假设 Range 表达式在循环开始之前被赋值了一次, 那么这是什么意思? 答案是它取决于数据类型, 所以让我们仔细看一下 Range 所支持的数据类型.

在我们这样做之前, 请记住这一点: 在 Go 中, 您分配的所有东西都被复制. 如果您分配一个指针, 则复制指针.如果你分配一个结构体, 则复制结构.将参数传递给函数时也是如此. 无论如何, 这里是:

// TODO

请参阅本文底部的参考资料, 了解更多关于这些数据类型的内部结构.

那么这是什么意思? 这些例子突出了一些差异:

// copies the entire array
var a [10]int
acopy := a

// copies the slice header struct only, NOT the backing array
s := make([]int, 10)
scopy := s

// copies the map pointer only
m := make(map[string]int)
mcopy := m

所以如果在一个 Range 循环的开始处, 你可以将一个数组表达式赋值给一个变量(以确保它只能计算一次), 那么你将复制整个数组.

Step 3: Go 编译器源码

(未完待续)

Previous
Next
Load Disqus

Unless otherwise credited all material Creative Commons License by Lingchao Xin