整理自 https://garbagecollected.org/2017/02/22/go-range-loop-internals/前端
下面这段程序会终止吗?golang
v := []int{1, 2, 3} for i := range v { v = append(v, i) }
第一件事就是去读关于 range loop
的文档。文档在 the for statement section "For statements with range
clause" 下。c#
先来个示例:数组
for i := range a { fmt.Println(i) }
range
左边变量(上面的i
)的赋值大部分使用下面两种形式:安全
=
):=
)你也能够忽略它。数据结构
若是你使用 :=
,Go在每次迭代时都会复用这个变量(仅在循环内部)。app
range
右边的(上面的 a
)你能够叫作 range
表达式。能够是下面几种:ide
array
slice
string
map
channel
,好比 chan int
or chan<- int
在循环前 range
表达式仅被求值一次。关于这条规则有一点须要注意:若是你遍历的是数组(或者数组的指针),而你仅仅获取索引,那么只有 len(a)
被计算。仅计算len(a)
意味着表达式a
可能在编译期就求值了,而后被编译器用一个常量替换。The spec for the len
function 里解释道:函数
若是
s
的类型是数组或者数组的指针,而且s
不包含channel
接收或者(很是量)函数调用,那么len(s)
和cap(s)
的值是常量值,这种状况下s
不会被求值。oop
你怎样才能调用一个表达式仅一次?经过把它赋值给一个变量。
有趣的是说明文档提到了一些关于增/删map
的(没有提到切片):
若是迭代期间删除了还没有到达的map条目,那么就不会产生相应的迭代值。若是迭代期间建立了map条目,该条目可能在迭代期间产生,也可能被跳过。
我稍后会说到map。
记住一点:在Go中,你赋值的一切都会拷贝。若是你赋值一个指针,你会拷贝指针,若是你赋值结构体,你也会拷贝结构体。把参数传给函数也是这样。
类型 | 对应的语法糖 |
---|---|
数组 | 就是数组 |
字符串 | 保存有长度字段和底层数组指针的结构体 |
切片 | 保存有长度、容量字段和底层数组指针的结构体 |
字典 | 一个结构体指针 |
channel | 一个结构体指针 |
请看博客下方了解这些数据类型的内部结构。
这是什么意思呢?这些例子高亮显示了一些不一样。
// 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
表达式开始你把一个数组赋值给一个变量(确保它只被求值一次),你将会拷贝整个数组。
懒惰的我简单的google了下Go编译器源码。我第一个找的是编译器的GCC版本。有趣的是下面的注释(在statements.cc
中):
// 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风格循环的语法糖。range支持的每种类型都有特定的语法糖。好比,数组:
// 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 // }
切片:
// 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 // }
共同的主题是:
这是在GCC前端。我知道的大多数人使用gc编译器做为Go的发布。看起来编译器作了差很少相同的事情。
掌握了这些后,让咱们回过头来看看博客开始处的例子。
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) }
咱们知道切片就是个语法糖,它是一个含有指向底层数组指针的结构体。循环在for_temp
上迭代,for_temp
是v
结构体的一个拷贝。变量v
的任何改变都不会影响另外一个结构体拷贝。结构体共享的是底层数组的指针,因此像v[i] = 1
这样的代码是能够正常工做的。
再一次,像上面例子展现的那样,数组会在循环开始以前被赋值给一个临时变量,这意味着将会拷贝整个数组。指针能够正常工做的缘由是拷贝的是指针值而不是数组。
在说明文档中,咱们看到:
为何会是这样?首先咱们知道,map是一个结构体的指针。在开始以前,拷贝的是指针而不是内部的数据结构,所以在循环内增删key是能够的。这是有道理的。
为何你在接下来的迭代中可能看不到你新加的元素?若是你知道hash表是怎么工做的(map实际上就是hash表),你应该知道在hash表内条目的顺序是不固定的。你新加的条目有可能被hash到0索引的位置。因此若是你假设Go会以任意顺序遍历数组,那么你是否会在循环内看到你新加的元素是没法预测的。毕竟你可能已经通过了0索引的位置。在Go map中是不肯定会发生什么的,仍是让编译器决定吧。