golang

Go Range内幕

2018-10-29  本文已影响95人  parker7

前言:翻译一篇国外小哥对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)
}

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
//   }

这里有一些公共点:

我们所知道的

回到前面

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是指向struct的指针。在循环开始之前,将复制指针而不是内部数据结构,因此可以在循环中添加或删除键。

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

总结

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

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

上一篇下一篇

猜你喜欢

热点阅读