[TOC]git
首先讓咱們來看兩段代碼github
func main() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } }
func IndexArray() { a := [...]int{1, 2, 3, 4, 5, 6, 7, 8} for i := range a { a[3] = 100 if i == 3 { fmt.Println("IndexArray", i, a[i]) } } } func IndexValueArray() { a := [...]int{1, 2, 3, 4, 5, 6, 7, 8} for i, v := range a { a[3] = 100 if i == 3 { fmt.Println("IndexValueArray", i, v) } } } func IndexValueArrayPtr() { a := [...]int{1, 2, 3, 4, 5, 6, 7, 8} for i, v := range &a { a[3] = 100 if i == 3 { fmt.Println("IndexValueArrayPtr", i, v) } } } func main() { IndexArray() IndexValueArray() IndexValueArrayPtr() }
咱們應該都知道,對於 range 左邊的循環變量能夠用如下方式來賦值:golang
等號直接賦值 (=) 短變量申明賦值 (:=) 固然也能夠什麼都不寫來徹底忽略迭代遍歷到的值。express
若是使用短變量申明(:=),Go 會在每次循環的迭代中重用申明的變量(只在循環內的做用域裏有效) 表達式左邊必須是可尋址的或者map索引表達式,若是表達式是channel,最多容許一個變量,其餘狀況下容許兩個變量。c#
range 右邊表達式的結果,能夠是如下這些數據類型:數組
range 表達式會在開始循環前被 evaluated 一次。但有一個例外狀況:app
若是對一個數組或者指向數組的指針作 range 而且最多隻有一個變量(只用到了數組索引):此時只有表達式長度 被 evaluated。frontend
這裏的 evaluated 究竟是什麼意思?很不幸文檔裏沒有找到相關的說明。固然我猜其實就是徹底的執行表達式直到其不能再被拆解。不管如何,最重要的是 range 表達式 在整個迭代開始前會被徹底的執行一次。那麼你會怎麼讓一個表達式只執行一次?把執行結果放在一個變量裏! range 表達式的處理會不會也是這麼作的?函數
有趣的是規範文檔裏提到了一些對 maps (沒有提到 slices) 作添加或刪除操做的狀況。oop
若是 map 中的元素在尚未被遍歷到時就被移除了,後續的迭代中這個元素就不會再出現。而若是 map 中的元素是在迭代過程當中被添加的,那麼在後續的迭代這個元素可能出現也可能被跳過。
若是咱們假設在循環開始以前會先把 range 表達式複製給一個變量,那咱們須要關注什麼?答案是表達式結果的數據類型,讓咱們更近一步的看看 range 支持的數據類型。
在咱們開始前,先記住:在 Go 裏,不管咱們對什麼賦值,都會被複制。若是賦值了一個指針,那咱們就複製了一個指針副本。若是賦值了一個結構體,那咱們就複製了一個結構體副本。往函數裏傳參也是一樣的狀況。好了,開始吧:
Range expression 1st value 2nd value array or slice a [n]E, *[n]E, or []E index i int a[i] E string s string type index i int see below rune map m map[K]V key k K m[k] V channel c chan E, <-chan E element e E
然而這些對於真正解決咱們的問題彷佛並無太大做用! 好,咱們先看一段代碼:
func main() { // 複製整個數組 var a [10]int acopy := a a[0] = 10 fmt.Println("a", a) fmt.Println("acopy", acopy) // 只複製了 slice 的結構體,並無複製成員指針指向的數組 s := make([]int, 10) s[0] = 10 scopy := s fmt.Println("s", s) fmt.Println("scopy", scopy) // 只複製了 map 的指針 m := make(map[string]int) mcopy := m m["0"] = 10 fmt.Println("m", m) fmt.Println("mcopy", mcopy) }
你們猜下這個程序的輸出結果是什麼,不賣關子了,直接上答案。
a [10 0 0 0 0 0 0 0 0 0] acopy [0 0 0 0 0 0 0 0 0 0] s [10 0 0 0 0 0 0 0 0 0] scopy [10 0 0 0 0 0 0 0 0 0] m map[0:10] mcopy map[0:10]
因此,若是要在 range 循環開始前把一個數組表達式賦值給一個變量(保證表達式只 evaluate 一次),就會複製整個數組。
看下gcc源碼發現,咱們關心的和 range 有關的部分出如今 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 // }
slice:
// The loop we generate: // 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 // } // // Using for_temp means that we don't need to check bounds when // fetching range_temp[index_temp].
他們的共同點是:
- 循環變量在每一次迭代中都被賦值並會複用。
- 能夠在迭代過程當中移除一個 map 裏的元素或者向 map 裏添加元素。添加的元素並不必定會在後續迭代中被遍歷到。
如今讓咱們回到開篇的例子。 1.答案是程序能夠正常結束運行。它其實能夠粗略的翻譯成相似下面的這段:
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) }
2.先看輸出結果
IndexArray 3 100 IndexValueArray 3 4 IndexValueArrayPtr 3 100
咱們知道切片其實是一個結構體的語法糖,這個結構體有着一個指向數組的指針成員。在循環開始前對這個結構體生成副本而後賦值給 for_temp,後面的循環其實是在對 for_temp 進行迭代。任何對於原始變量 v 自己(而非對其背後指向的數組)的更改都和生成的副本 for_temp 沒有關係。但其背後指向的數組仍是以指針的形式共享給 v 和 for_temp,因此 v[i] = 1 這樣的語句仍然能夠工做。 和上面的例子相似,在循環開始前數組被賦值給了一個臨時變量,在對數組作 range 循環時臨時變量裏存放的是整個數組的副本,對原數組的操做不會反映在副本上。而在對數組指針作 range 循環時臨時變量存放的是指針的副本,操做的也是同一塊內存空間。
下面讓咱們再來一個例子,看看你是否真正理解了。
type Foo struct { bar string } func main() { list := []Foo{ {"A"}, {"B"}, {"C"}, } list2 := make([]*Foo, len(list)) for i, value := range list { list2[i] = &value } fmt.Println(list[0], list[1], list[2]) fmt.Println(list2[0], list2[1], list2[2]) }
在這個例子中,咱們幹了下面的一些事情:
從代碼來看,理所固然,咱們指望獲得的結果應該是這樣:
{A} {B} {C} &{A} &{B} &{C}
可是結果卻出乎意料,程序的輸出是這樣的:
{A} {B} {C} &{C} &{C} &{C}
在Go的for…range循環中,Go始終使用值拷貝的方式代替被遍歷的元素自己,簡單來講,就是for…range中那個value,是一個值拷貝,而不是元素自己。這樣一來,當咱們指望用&獲取元素的地址時,實際上只是取到了value這個臨時變量的地址,而非list中真正被遍歷到的某個元素的地址。而在整個for…range循環中,value這個臨時變量會被重複使用,因此,在上面的例子中,list2被填充了三個相同的地址,其實都是value的地址。而在最後一次循環中,value被賦值爲{c}。所以,list2輸出的時候顯示出了三個&{c}。
一樣的,下面的寫法,跟for…range的例子一模一樣:
var value Foo for var i := 0; i < len(list); i++ { value = list[i] list2[i] = &value }
那麼,怎樣纔是正確的寫法呢?咱們應該用index來訪問for…range中真實的元素,並獲取其指針地址:
for i, _ := range list { list2[i] = &list[i] }
這樣,輸出list2中的元素,就能獲得咱們想要的結果(&{A} &{B} &{C})了。