Golang 里的 range 问题

Golang 类似于 C/C++,同时又从其他语言(比如 Python)里借鉴了一些语法,比如 range。

在使用上,range 有一些特性,如果不注意的话,很容易引起误解。比如以下这段代码,会不会一直循环下去?

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

先说结论:不会,循环运行 3 次后结束。

细节解释:

  1. range 时,先对 v 进行了一次浅拷贝(包括指向底层数组某个偏移位置的指针、切片元素的长度、切片可以使用的总长度等),并用这份拷贝来进行 for 的 3 次循环;
  2. 第一次 append 时,因为底层数组的容量不够,所以会分配更大的(由运行时的算法决定的)内存,把原有数据拷贝进去,并通过返回值更新 v 的内容,也就是说,这时 v 已经有了新的指向。(当然,在刚开始时,v 还是指向了原来的内存,所以这时可以对原来的数组进行访问和修改);
  3. 第二次 append 时,如果当前 v 指向的的底层数组足够大,那么直接追加;否则类似上一个步骤里的做法;
  4. 第三次 append 时,同上一步;
  5. 循环结束;

可以通过这样的代码,来跟踪具体的运行过程,特别是 v 被重新分配时候:

 func main() {
     v := []int{1, 2, 3}
     for i := range v {
+        fmt.Printf("v: len:%d, cap:%d\n", len(v), cap(v))
         v = append(v, i)
+        fmt.Printf("v: len:%d, cap:%d\n", len(v), cap(v))
     }
+    fmt.Printf("v: %v, len:%d, cap:%d\n", v, len(v), cap(v))
 }

运行结果(实际的容量变化取决于 go 运行时的算法):

v: len:3, cap:3
v: len:4, cap:6
v: len:4, cap:6
v: len:5, cap:6
v: len:5, cap:6
v: len:6, cap:6
v: [1 2 3 0 1 2], len:6, cap:6

所以,需要注意 range 时的那次浅拷贝,以及切片在 append 时可能引起的底层数组的重新分配。

以下是一些参考文章,里面提到的内容,对于深入理解 range 的实现及应用很有帮助:

Read More: