Go Quiz: 從Go面試題搞懂slice range遍歷的坑

coding進階發表於2022-01-28

面試題

最近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種情況:

  • 如果切片的容量足夠,就會在切片指向的底層陣列裡追加元素。
  • 如果切片的容量不足以承載新新增的元素,就會開闢一個新的底層陣列,把原切片裡的元素拷貝過來,再追加新的元素。切片結構裡的指標會指向新的底層陣列。

答案

我們回到本文最開始的題目,逐行解析每行程式碼的執行結果。

程式碼程式執行結果
var x = []string{"A", "B", "C"}x是一個切片,長度是3,容量是3,x指向的底層陣列的值是[ "A" "B" "C"]
for i, s := range x編譯器先構造一個切片x的拷貝,假設為切片y,然後對y做遍歷。y的值在for迴圈執行前就確定下來了,長度為3,容量為3,固定不變。
print(i, s, ",")第一次for迴圈,i的值是0,s的值是切片y裡下標索引為0的元素,值為"A",列印0A
x[i+1] = "M"執行x[1] = "M",因為切片xy現在指向同一個底層陣列,切片y裡下標索引為1的元素的值也被改成了"M",y指向的底層陣列的值為["A", "M", "C"]
x = append(x, "Z")給切片x新增新元素"Z",因為當前切片x的長度為3,容量為3,容量已滿,不足以承載新增加的元素,所以要對x的底層陣列做擴容,x指向新的底層陣列,新底層陣列的值是["A", "M", "C", "Z"],y還是指向原來的底層陣列,y指向的底層陣列的值是["A", "M", "C"]
後續for迴圈因為從第2次for迴圈開始,xy指向了不同的底層陣列,所以對切片x的修改不會影響到y,因此後面列印的結果依次是1M,2C

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

總結

對於slice,時刻想著對slice做了修改後,slice裡的3個欄位:指標,長度,容量是怎麼變的。

​ zen of go

  • 對切片x做range遍歷,實際上是對x的拷貝(假設為y)做range遍歷,y的值(包括y結構體裡指向底層陣列的指標的值,y的長度和容量)都在執行for迴圈前確定下來了。
  • 切片的底層資料結構和擴容機制,如果有不清楚的,參考我寫的slice底層原理篇,包含了slice的所有注意事項。

開源地址

文章和示例程式碼開源地址在GitHub: https://github.com/jincheng9/...

公眾號:coding進階

個人網站:https://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

相關文章