整理自 https://garbagecollected.org/2017/02/22/go-range-loop-internals/前端
下面這段程序會終止嗎?golang
v := []int{1, 2, 3} for i := range v { v = append(v, i) }
第一件事就是去讀關於 range loop
的文檔。文檔在 the for statement section "For statements with range
clause" 下。c#
先來個示例:數組
for i := range a { fmt.Println(i) }
range
左邊變量(上面的i
)的賦值大部分使用下面兩種形式:安全
=
):=
)你也能夠忽略它。數據結構
若是你使用 :=
,Go在每次迭代時都會複用這個變量(僅在循環內部)。app
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。
記住一點:在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
表達式開始你把一個數組賦值給一個變量(確保它只被求值一次),你將會拷貝整個數組。
懶惰的我簡單的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 // }
共同的主題是:
這是在GCC前端。我知道的大多數人使用gc編譯器做爲Go的發佈。看起來編譯器作了差很少相同的事情。
掌握了這些後,讓咱們回過頭來看看博客開始處的例子。
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_temp
是v
結構體的一個拷貝。變量v
的任何改變都不會影響另外一個結構體拷貝。結構體共享的是底層數組的指針,因此像v[i] = 1
這樣的代碼是能夠正常工做的。
再一次,像上面例子展現的那樣,數組會在循環開始以前被賦值給一個臨時變量,這意味着將會拷貝整個數組。指針能夠正常工做的緣由是拷貝的是指針值而不是數組。
在說明文檔中,咱們看到:
爲何會是這樣?首先咱們知道,map是一個結構體的指針。在開始以前,拷貝的是指針而不是內部的數據結構,所以在循環內增刪key是能夠的。這是有道理的。
爲何你在接下來的迭代中可能看不到你新加的元素?若是你知道hash表是怎麼工做的(map實際上就是hash表),你應該知道在hash表內條目的順序是不固定的。你新加的條目有可能被hash到0索引的位置。因此若是你假設Go會以任意順序遍歷數組,那麼你是否會在循環內看到你新加的元素是沒法預測的。畢竟你可能已經通過了0索引的位置。在Go map中是不肯定會發生什麼的,仍是讓編譯器決定吧。