Go Range循環的真相

Go Range Loop Internals

整理自 https://garbagecollected.org/2017/02/22/go-range-loop-internals/前端

下面這段程序會終止嗎?golang

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

Step 1: 請閱讀該死的使用手冊

第一件事就是去讀關於 range loop 的文檔。文檔在 the for statement section "For statements with range clause" 下。c#

先來個示例:數組

for i := range a {
    fmt.Println(i)
}

range變量

range 左邊變量(上面的i)的賦值大部分使用下面兩種形式:安全

  • 賦值(=
  • 短變量聲明(:=

你也能夠忽略它。數據結構

若是你使用 := ,Go在每次迭代時都會複用這個變量(僅在循環內部)。app

range 表達式

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。

Step 2: range支持的數據類型

記住一點:在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 表達式開始你把一個數組賦值給一個變量(確保它只被求值一次),你將會拷貝整個數組。

Step 3: Go編譯器源碼

懶惰的我簡單的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
//   }

共同的主題是:

  • 全部的一切都只是C風格的循環。
  • 你迭代的東西被賦值給一個臨時變量。

這是在GCC前端。我知道的大多數人使用gc編譯器做爲Go的發佈。看起來編譯器作了差很少相同的事情。

咱們瞭解的

  1. 循環變量是複用的而且每次迭代都被賦值。
  2. range表達式在循環開始前被求值一次,並賦值給一個變量。
  3. 迭代map時你能夠刪除或者添加值。添加的值可能會也可能不會出如今循環中。

掌握了這些後,讓咱們回過頭來看看博客開始處的例子。

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_tempv結構體的一個拷貝。變量v的任何改變都不會影響另外一個結構體拷貝。結構體共享的是底層數組的指針,因此像v[i] = 1這樣的代碼是能夠正常工做的。

再一次,像上面例子展現的那樣,數組會在循環開始以前被賦值給一個臨時變量,這意味着將會拷貝整個數組。指針能夠正常工做的緣由是拷貝的是指針值而不是數組。

附:maps

在說明文檔中,咱們看到:

  • 在迭代字典時添加或者刪除元素是安全的。
  • 若是你添加了一個元素,這個元素可能會也可能不會出如今下次迭代中。

爲何會是這樣?首先咱們知道,map是一個結構體的指針。在開始以前,拷貝的是指針而不是內部的數據結構,所以在循環內增刪key是能夠的。這是有道理的。

爲何你在接下來的迭代中可能看不到你新加的元素?若是你知道hash表是怎麼工做的(map實際上就是hash表),你應該知道在hash表內條目的順序是不固定的。你新加的條目有可能被hash到0索引的位置。因此若是你假設Go會以任意順序遍歷數組,那麼你是否會在循環內看到你新加的元素是沒法預測的。畢竟你可能已經通過了0索引的位置。在Go map中是不肯定會發生什麼的,仍是讓編譯器決定吧。

參考

  1. The Go Programming Language Specification
  2. Go slices: usage and internals
  3. Go Data Structures
  4. Inside the map implementation: slides | video
  5. Understanding nil: slides | video
  6. string source code
  7. slice source code
  8. map source code
  9. channel source code
相關文章
相關標籤/搜索