Go Quiz: 从Go面试题搞懂slice range遍历的坑

518 阅读2分钟

面试题

最近Go 101的作者发布了11道Go面试题,非常有趣,打算写一个系列对每道题做详细解析,欢迎大家关注。

本题是Go quiz slice系列的第2道题目,这道题非常有迷惑性。

通过这道题我们可以知晓对slice做range遍历的坑,避免在实际项目中踩坑。

package main

func main() {
    var x = []string{"A", "B", "C"}

    for i, s := range x {
        print(i, s, ",")
        x[i+1] = "M"
        x = append(x, "Z")
        x[i+1] = "Z"
    }
}
  • 0A,1B,2C,
  • 0A,1Z,2Z,
  • 0A,1M,2M,
  • 0A,1M,2C,
  • 0A,1Z,2M,
  • 0A,1M,2Z,
  • (infinite loop)

大家可以在评论区留下你们的答案。这道题主要有以下几个考点:

  1. slice做range遍历,Go编译器背后会做哪些事情?
  2. slice什么时候扩容,扩容后的行为是怎么样的?

解析

我们先逐个解答上面的问题。

range遍历机制

range对slice做遍历的时候,实际上是先构造一个原slice的拷贝,再对这个拷贝做遍历。

在for循环里面的逻辑执行之前,这个拷贝的值就确定下来了。因此这个拷贝的长度和容量是不会在for循环的时候发生改变的。

以上面的题目为例:range x 实际上是会先构造一个原切片x的拷贝,我们假设为y,然后对y做遍历。

for i, s := range x {
        print(i, s, ",")
        x[i+1] = "M"
        x = append(x, "Z")
        x[i+1] = "Z"
}

上面这段代码可以等价为:

y := x
for i := 0; i < len(y); i++ {
    print(i, y[i], ",")
    x[i+1] = "M"
    x = append(x, "Z")
    x[i+1] = "Z"
}

slice扩容机制

通过append函数给slice添加元素的时候,有2种情况:

  • 如果切片的容量足够,就会在切片指向的底层数组里追加元素。
  • 如果切片的容量不足以承载新添加的元素,就会开辟一个新的底层数组,把原切片里的元素拷贝过来,再追加新的元素。切片结构里的指针会指向新的底层数组。

答案

我们回到本文最开始的题目,逐行解析每行代码的执行结果。

所以本题的答案是 0A, 1M, 2C。

总结

对于slice,时刻想着对slice做了修改后,slice里的3个字段:指针,长度,容量是怎么变的。

  • 对切片x做range遍历,实际上是对x的拷贝(假设为y)做range遍历,y的值(包括y结构体里指向底层数组的指针的值,y的长度和容量)都在执行for循环前确定下来了。
  • 切片的底层数据结构和扩容机制,如果有不清楚的,参考我写的slice底层原理篇,包含了slice的所有注意事项。

开源地址

文章和示例代码开源地址在GitHub: github.com/jincheng9/.…

公众号:coding进阶

个人网站:jincheng9.github.io/

思考题

留下2道思考题,欢迎大家在评论区留下你们的答案。也可以在我的wx公号发送消息slice2获取答案和原因。

  • 题目1:

    package main
    
    func main() {
        var x = []string{"A", "B", "C"}
    
        for i, s := range x {
            print(i, s, " ")
            x = append(x, "Z")
            x[i+1] = "Z"
        }
    }
    
  • 题目2

    package main
    
    func main() {
        var y = []string{"A", "B", "C", "D"}
        var x = y[:3]
    
        for i, s := range x {
            print(i, s, ",")
            x = append(x, "Z")
            x[i+1] = "Z"
        }
    }
    

References