為什麼 Go for-range 的 value 值地址每次都一樣?

2023-04-30 18:01:03

原文連結: 為什麼 Go for-range 的 value 值地址每次都一樣?

迴圈語句是一種常用的控制結構,在 Go 語言中,除了 for 關鍵字以外,還有一個 range 關鍵字,可以使用 for-range 迴圈迭代陣列、切片、字串、map 和 channel 這些資料型別。

但是在使用 for-range 迴圈迭代陣列和切片的時候,是很容易出錯的,甚至很多老司機一不小心都會在這裡翻車。

具體是怎麼翻的呢?我們接著看。

現象

先來看兩段很有意思的程式碼:

無限迴圈

如果我們在遍歷陣列的同時向陣列中新增元素,能否得到一個永遠都不會停止的迴圈呢?

比如下面這段程式碼:

func main() {
    arr := []int{1, 2, 3}
    for _, v := range arr {
        arr = append(arr, v)
    }
    fmt.Println(arr)
}

程式輸出:

$ go run main.go
1 2 3 1 2 3

上述程式碼的輸出意味著迴圈只遍歷了原始切片中的三個元素,我們在遍歷切片時追加的元素並沒有增加回圈的執行次數,所以迴圈最終還是停了下來。

相同地址

第二個例子是使用 Go 語言經常會犯的一個錯誤。

當我們在遍歷一個陣列時,如果獲取 range 返回變數的地址並儲存到另一個陣列或者雜湊時,會遇到令人困惑的現象:

func main() {
    arr := []int{1, 2, 3}
    newArr := []*int{}
    for _, v := range arr {
        newArr = append(newArr, &v)
    }
    for _, v := range newArr {
        fmt.Println(*v)
    }
}

程式輸出:

$ go run main.go
3 3 3

上述程式碼並沒有輸出 1 2 3,而是輸出 3 3 3

正確的做法應該是使用 &arr[i] 替代 &v,像這種程式設計中的細節是很容易出錯的。

原因

具體原因也並不複雜,一句話就能解釋。

對於陣列、切片或字串,每次迭代,for-range 語句都會將原始值的副本傳遞給迭代變數,而非原始值本身。

口說無憑,具體是不是這樣,還得靠原始碼說話。

Go 編譯器會將 for-range 語句轉換成類似 C 語言的三段式迴圈結構,就像這樣:

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

迭代陣列時,是這樣:

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

從上面的程式碼片段,可以總結兩點:

  1. 在迴圈開始前,會將陣列或切片賦值給一個新變數,在賦值過程中就發生了拷貝,迭代的實際上是副本,這也就解釋了現象 1。
  2. 在迴圈過程中,會將迭代元素賦值給一個臨時變數,這又發生了拷貝。如果取地址的話,每次都是一樣的,都是臨時變數的地址。

以上就是本文的全部內容,如果覺得還不錯的話歡迎點贊轉發關注,感謝支援。


參考文章:

推薦閱讀: