Go Range内幕


前言:翻译一篇国外小哥对Range的分析...

原文链接:
go-range loop internals

我们都知道Range使用非常方便,但是我总是可以发现Range有点神秘。而且并不是我一个人这么认为(me too):

  # 这个程序会无限循环吗?
  func main() {
    v := []int{1, 2, 3}
    for i := range v {
      v = append(v, i)
    }
  }
  — Dαve Cheney (@davecheney) January 13, 2017

现在我准备根据这些事实来探索Range中间发生了什么,并且会分步骤将这些都记录下来。

Step 1:

第一个任务就是读go语言规范文档。for语句部分 “For statements with rangeclause” 下的范围循环。
我这里不会复制整个文档,但是会记录其中有趣的内容。

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

for i := range a {
    fmt.Println(i)
}
  • range 变量
    大多数人都知道在range子句的左边(在上面的例子中的i)你可以使用以下命令分配循环变量:
    -- 赋值(=)
    -- 短变量声明(:=)
    你也可以选择不做任何事情来完全忽略循环变量。
    如果使用短变量声明(:=),Go将为循环的每次迭代重用变量(仅在循环的范围内)
  • range 表达式
    在range子句的右侧(在上面的示例中a),你可以找到他们称之为range表达式的内容。它可以包含任何计算结果为以下之一的表达式:
    -- array
    -- pointer to an array
    -- slice
    -- string
    -- map
    -- channels that allow receiving, e.g. chan int or chan<- int
    在开始循环之前,range表达式被计算一次。
    ⚠️这里有一个例外:如果表达式是一个数组(或指针),那么只会len(a)对其进行求值。只对len(a)求值意味着表达式a可以在编译时进行求值,并有编译器替换为常量。len语言规范
    The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated. Otherwise, invocations of len and cap are not constant and s is evaluated.
    文档中“evaluated”说的不是很清楚,在其他规范中也找不到相关信息。当然我可以猜测它完全执行表达式,知道它无法进一步减少。在任何情况下,这里的高阶位是在循环开始之前对表达式进行了一次求值。
    那么你如何对表达式进行求值?通过将其赋值为变量!这就是这里发生的事吗?
    有趣的是,Go语言规范文档中有关于添加、删除map描述:(没有slices)
    If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped.
    稍后我们将回到map上。

Step 2:range 支持数据类型

如果我们假设在循环开始之前被赋值给变量一次,这意味着什么?答案是取决于变量类型,所以让我们仔细看看range支持的数据类型。
在这样做之前,请记住:in Go, everything you assign, you copy.(值传递)如下表:

数据类型 语法糖
array the array
string struct holding len + a pointer to the backing array
slice struct holding len, cap + a pointer to the backing array
map pointer to a struct
channel pointer to a struct

请参阅本文底部的参考资料,以了解有关这些数据类型的内部结构的更多信息。
那么这是什么意思?看下面的例子不同之处:

// 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源码

在Go源码中,这个文件 statements.cc,就for而言正如注释所述:

// Arrange to do a loop appropriate for the type.  We will produce
//   for INIT ; COND ; POST {
//           ITER_INIT
//           INDEX = INDEX_TEMP
//           VALUE = VALUE_TEMP // If there is a value
//           original statements
//   }

range循环内部只是c风格循环的语法糖。例如:
Array

// The loop we generate:
//   len_temp := len(range)
//   range_temp := range
//   for index_temp = 0; index_temp < len_temp; index_temp++ {
//           value_temp = range_temp[index_temp]
//           index = index_temp
//           value = value_temp
//           original body
//   }

Slice

//   for_temp := range
//   len_temp := len(for_temp)
//   for index_temp = 0; index_temp < len_temp; index_temp++ {
//           value_temp = for_temp[index_temp]
//           index = index_temp
//           value = value_temp
//           original body
//   }

这里有一些公共点:

  • 这都是c风格循环
  • 迭代时被分配给一个临时变量

我们所知道的

  • 1.循环变量在每次迭代时被重用并分配给它们。
  • 2.通过赋值给变量,在循环开始之前对范围表达式求值一次。
  • 3.你可以在迭代时添加或删除map的值,循环中可能会显示添加内容,也可能不会显示。

回到前面

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

这个程序会被终止原因是因为它被翻译成了下面的代码:

for_temp := v
len_temp := len(for_temp)
for index_temp = 0; index_temp < len_temp; index_temp++ {
        value_temp = for_temp[index_temp]
        index = index_temp
        value = value_temp
        v = append(v, index)
}

我们知道slice是一个包含指向底层数组的指针的结构的语法糖。循环迭代len_temp,这是在循环开始之前就获取到的结构副本。因此,变量本身的更改并不相关,因为它是结构的另外一个副本。支持的数组仍然是共享的,因为它只是该结构中的指针,所以类似 v[i] = 1 还是可以的。

其他:map

在Go语言规范中,我们知道:

  • 在循环中添加或删除map是安全的
  • 如果你添加一个map元素,它可能会或者不会在下面的循环中看到它

它为什么这么工作?首先,我们知道map是指向struct的指针。在循环开始之前,将复制指针而不是内部数据结构,因此可以在循环中添加或删除键。

那为什么在下面的循环中看不到添加的map元素内?好吧,如果你知道哈希表是如何工作的,map就是如此,在哈希表的支持数组中,元素没有特定顺序。你最后添加的元素可能会在数组中散列为索引零。因此,如果您假设Go保留以任何顺序迭代此数组的权利,则确实无法预测您是否会在循环内看到您添加的项目。毕竟,您可能已经在支持数组中超过索引零​​。这可能与Go映射的情况不完全相同,但出于这个原因将决策留给编译器编写器是有意义的。

总结

步骤清晰,思路明确,还是引申扩展,写的很好,对于新手来说这也是一个容易犯错的地方。

string source code
slice source code
map source code
channel source code

推荐阅读更多精彩内容

  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 11,881评论 1 43
  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 83,450评论 26 524
  • 原文:Go Range Loop Internals Go 里的 range 循环用起来非常方便,但我总觉得它在不...
    Newt0n阅读 2,342评论 0 10
  • 一切都在变, 唯一不变就是变, 一切都没有变, 是的, 变与不变,好像是一个好命题。
    冯军宏阅读 80评论 0 0
  • 文/雪诺 公众号:迷茫人生路 最近被一只旅行的青蛙给刷爆了朋友圈,这款游戏用当下流行的词语来形容就是:“佛系...
    Snow凤阅读 67评论 0 0