Go Range 循环内幕
虽然他们非常方便, 但我总是发现 Go 的 Range 循环有点神秘. 我并不是第一个:
// http://bit.ly/2CXC1Ob 来自 Dave Cheney. |
现在我可以把这些事实记录下来, 但是我很可能会忘记. 为了有更好的机会记住这个, 我需要找出为什么 range 循环会这样. 所以我写了这篇文章.
Step 1: 读手册(RTFM)
我们首先应该去读 range 循环文档. Go语言规范文档在 for 语句部分的 For 语句和 range 子句描述了 range 循环. 我不会在这里复制整个规范,我会总结一些有趣的部分.
首先, 让我们提醒自己我们在这里看到什么:
for i := range a { |
Range 变量
你们中的大多数人会知道, 在 Range 子句的左边(上面的例子中的 i), 你可以这样分配循环变量:
- 分配 (=)
- 短变量声明 (:=)
您也可以选择完全忽略循环变量.
如果使用短变量声明样式分配(:=), 则 Go 将在循环的每个迭代中重用变量(仅在循环内的范围内).
Range 表达式
在 Range 子句的右边(上面的例子中的 a), 你可以找到他们称之为 Range 表达式的东西. 它可以包含任何表达式, 其计算结果如下:
- 数组(array)
- 指向数组的指针
- 切片(slice)
- 字符串(string)
- 字典(map)
- 允许接收的管道(channel), 如: chan int 或者 chan<- int
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 |
所以如果在一个 Range 循环的开始处, 你可以将一个数组表达式赋值给一个变量(以确保它只能计算一次), 那么你将复制整个数组.
Step 3: Go 编译器源码
(未完待续)