這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
以下所有觀點都是個人愚見,有不同建議或補充的的歡迎emialaboutme
原文章地址
關於for語句的疑問
for語句的規範
for語句的內部實現-array
問題解答
關於for語句的疑問
我們都知道在golang中,迴圈語句只有for這一個,在代碼中寫一個迴圈都一般都需要用到for(當然你用goto也是可以的),雖然golang的for語句很方便,但不少初學者一樣對for語句持有不少疑問,如:
- for語句一共有多少種運算式格式?
- for語句中臨時變數是怎麼回事?(為什麼有時遍曆賦值後,所有的值都等於最後一個元素)
- range後面支援的資料類型有哪些?
- range string類型為何得到的是rune類型?
- 遍曆slice的時候增加或刪除資料會怎麼樣?
- 遍曆map的時候增加或刪除資料會怎麼樣?
其實這裡的很多疑問都可以看golang程式設計語言規範,有興趣的同學完全可以自己看,然後根據自己的理解來解答這些問題。
for語句的規範
for語句的功能用來指定重複執行的語句塊,for語句中的運算式有三種:
官方的規範: ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
- Condition = Expression .
- ForClause = [ InitStmt ] “;” [ Condition ] “;” [ PostStmt ] .
- RangeClause = [ ExpressionList “=” | IdentifierList “:=” ] “range” Expression .
單個條件判斷
形式:
for a < b { f(doThing)}// or 省略運算式,等價於truefor { // for true { f(doThing)}
這種格式,只有單個邏輯運算式, 邏輯運算式的值為true,則繼續執行,否則停止迴圈。
for語句中兩個分號
形式:
for i:=0; i < 10; i++ { f(doThing)}// orfor i:=0; i < 10; { i++ f(doThing)}// or var i intfor ; i < 10; { i++ f(doThing)}
這種格式,語氣被兩個分號分割為3個運算式,第一個表示為初始化(只會在第一次條件運算式之計算一次
),第二個運算式為條件判斷運算式,第三個運算式一般為自增或自減,但這個運算式可以任何符合文法的運算式。而且這三個運算式,只有第二個運算式是必須有的,其他運算式可以為空白。
for和range結合的語句
形式:
for k,v := range []int{1,2,3} { f(doThing)}// or for k := range []int{1,2,3} { f(doThing)}// orfor range []int{1,2,3} { f(doThing)}
用range來迭代資料是最常用的一種for語句,range右邊的運算式叫範圍運算式
,範圍運算式可以是數組,數組指標,slice,字串,map和channel。因為要賦值,所以左側的運算元(也就是迭代變數)必須要可定址的,或者是map下標的運算式。如果迭代變數是一個channel,那麼只允許一個迭代變數,除此之外迭代變數可以有一個或者兩個。
範圍運算式在開始迴圈之前只進行一次求值,只有一個例外:如果範圍運算式是數組或指向數組的指標,至多有一個迭代變數存在,只對範圍運算式的長度進行求值;如果長度為常數,範圍運算式本身將不被求值。
每迭代一次,左邊的函數調用求值。對於每個迭代,如果相應的迭代變數存在,則迭代值如下所示產生:
Range expression 1st value 2nd valuearray or slice a [n]E, *[n]E, or []E index i int a[i] Estring s string type index i int see below runemap m map[K]V key k K m[k] Vchannel c chan E, <-chan E element e E
- 對於數組、數組指標或是分區值a來說,下標迭代值升序產生,從0開始。有一種特殊情境,只有一個迭代參數存在的情況下,range迴圈產生0到len(a)的迭代值,而不是索引到數組或是分區。對於一個nil分區,迭代的數量為0。
- 對於字串類型,range子句迭代字串中每一個Unicode代碼點,從下標0開始。在連續迭代中,下標值會是下一個utf-8代碼點的第一個位元組的下標,而第二個實值型別是rune,會是對應的代碼點。如果迭代遇到了一個非法的Unicode序列,那麼第二個值是0xFFFD,也就是Unicode的替換字元,然後下一次迭代只會前進一個位元組。
- map中的迭代順序是沒有指定的,也不保證兩次迭代是一樣的。如果map元素在迭代過程中被刪掉了,那麼對應的迭代值不會再產生。如果map元素在迭代中插入了,則該元素可能在迭代過程中產生,也可能被跳過,但是每個元素的迭代值頂多出現一次。如果map是nil,那麼迭代次數為0。
- 對於管道,迭代值就是下一個send到管道中的值,除非管道被關閉了。如果管道是nil,範圍運算式永遠阻塞。
迭代值會賦值給相應的迭代變數,就像是指派陳述式。
迭代變數可以使用短變數聲明(:=)。這種情況,它們的類型設定為相應迭代值的類型,它們的域是到for語句的結尾,它們在每一次迭代中複用。如果迭代變數是在for語句外聲明的,那麼執行之後它們的值是最後一次迭代的值。
var testdata *struct {a *[7]int}for i, _ := range testdata.a {// testdata.a is never evaluated; len(testdata.a) is constant// i ranges from 0 to 6f(i)}var a [10]stringfor i, s := range a {// type of i is int// type of s is string// s == a[i]g(i, s)}var key stringvar val interface {} // value type of m is assignable to valm := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}for key, val = range m {h(key, val)}// key == last map key encountered in iteration// val == map[key]var ch chan Work = producer()for w := range ch {doWork(w)}// empty a channelfor range ch {}
for語句的內部實現-array
golang的for語句,對於不同的格式會被編譯器編譯成不同的形式,如果要弄明白需要看golang的編譯器和相關資料結構的源碼,資料結構源碼還好,但是編譯器是用C++寫的,本人C++是個弱雞,這裡只講array內部實現。
// 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// }// 例如代碼: array := [2]int{1,2}for k,v := range array { f(k,v)}// 會被編譯成: len_temp := len(array)range_temp := arrayfor index_temp = 0; index_temp < len_temp; index_temp++ { value_temp = range_temp[index_temp] k = index_temp v = value_temp f(k,v)}
所以像遍曆一個數組,最後產生的程式碼很像C語言中的遍曆,而且有兩個臨時變數index_temp
,value_temp
,在整個遍曆中一直複用這兩個變數。所以會導致開頭問題2的問題(詳細解答會在後邊)。
問題解答
for語句一共有多少種運算式格式?
這個問題應該很簡單了,上面的規範中就有答案了,一共有3種:
Condition = Expression .ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
for語句中臨時變數是怎麼回事?(為什麼有時遍曆賦值後,所有的值都等於最後一個元素)
先看這個例子:
var a = make([]*int, 3)for k, v := range []int{1, 2, 3} { a[k] = &v}for i := range a { fmt.Println(*a[i])}// result: // 3 // 3 // 3
由for語句的內部實現-array可以知道,即使是短聲明的變數,在for迴圈中也是複用的,這裡的v
一直都是同一個零時變數,所以&v
得到的地址一直都是相同的,如果不信,你可以列印該地址,且該地址最後存的變數等於最後一次迴圈得到的變數,所以結果都是3。
range後面支援的資料類型有哪些?
共5個,分別是數組,數組指標,slice,字串,map和channel
range string類型為何得到的是rune類型?
這個問題在for規範中也有解答,對於字串類型,在連續迭代中,下標值會是下一個utf-8代碼點的第一個位元組的下標,而第二個實值型別是rune。如果迭代遇到了一個非法的Unicode序列,那麼第二個值是0xFFFD,也就是Unicode的替換字元,然後下一次迭代只會前進一個位元組。
其實看完這句話,我沒理解,當然這句話告訴我們了遍曆string得到的第二個實值型別是rune,但是為什麼是rune類型,而不是string或者其他類型?後來在看了Rob Pike寫的blogStrings, bytes, runes and characters in Go才明白點,首先需要知道rune
是int32
的別名,且go語言中的字串字面量始終儲存有效UTF-8序列。而UTF-8就是用4位元組來表示Unicode字元集。所以go的設計者用rune表示單個字元的編碼,則可以完成容納所表示Unicode字元。舉個例子:
s := `漢語ab`fmt.Println("len of s:", len(s))for index, runeValue := range s { fmt.Printf("%#U starts at byte position %d\n", runeValue, index)}// result// len of s: 8// U+6C49 '漢' starts at byte position 0// U+8BED '語' starts at byte position 3// U+0061 'a' starts at byte position 6// U+0062 'b' starts at byte position 7
根據結果得知,s的長度是為8位元組,一個漢子佔用了3個位元組,一個英文字母佔用一個位元組,而程式go程式是怎麼知道漢子佔3個位元組,而英文字母佔用一個位元組,就需要知道utf-8代碼點的概念,這裡就不深究了,知道go是根據utf-8代碼點來知道該字元佔了多少位元組就ok了。
遍曆slice的時候增加或刪除資料會怎麼樣?
由for語句的內部實現-array可以知道,擷取slice的長度只在迴圈外執行了一次,該長度決定了遍曆的次數,不管在迴圈裡你怎麼改。但是對索引求值是在每次的迭代中求值的,如果更改了某個元素且該元素還未遍曆到,那麼最終遍曆得到的值是更改後的。刪除元素也是屬於更改元素的一種情況。
在slice中增加元素,會更改slice含有的元素,但不會更改遍曆次數。
a2 := []int{0, 1, 2, 3, 4}for i, v := range a2 { fmt.Println(i, v) if i == 0 { a2 = append(a2, 6) }}// result// 0 0// 1 1// 2 2// 3 3// 4 4
在slice中刪除元素,能刪除該元素,但不會更改遍曆次數。
// 只刪除該元素1,不更改slice長度a2 := []int{0, 1, 2, 3, 4}for i, v := range a2 { fmt.Println(i, v) if i == 0 { copy(a2[1:], a2[2:]) }}// result// 0 0// 1 2// 2 3// 3 4// 4 4// 刪除該元素1,並更改slice長度a2 := []int{0, 1, 2, 3, 4}for i, v := range a2 { fmt.Println(i, v) if i == 0 { copy(a2[1:], a2[2:]) a2 = a2[:len(a2)-2] //將a2的len設定為3,但並不會影響臨時slice-range_temp }}// result// 0 0// 1 2// 2 3// 3 4// 4 4
遍曆map的時候增加或刪除資料會怎麼樣?
規範中也有答案,map元素在迭代過程中被刪掉了,那麼對應的迭代值不會再產生。如果map元素在迭代中插入了,則該元素可能在迭代過程中產生,也可能被跳過。
在遍曆中刪除元素
m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5: 5}del := falsefor k, v := range m { fmt.Println(k, v) if !del { delete(m, 2) del = true }}// result// 4 4// 5 5// 1 1// 3 3
在遍曆中增加元素,多執行幾次看結果
m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5: 5}add := falsefor k, v := range m { fmt.Println(k, v) if !add { m[6] = 6 m[7] = 7 add = true }}// result1// 1 1// 2 2// 3 3// 4 4// 5 5// 6 6// result2// 1 1// 2 2// 3 3// 4 4// 5 5// 6 6// 7 7
在map遍曆中刪除元素,將會刪除該元素,且影響遍曆次數,在遍曆中增加元素則會有不可控的現象出現,有時能遍曆到新增的元素,有時不能。具體原因下次分析。
參考
https://golang.org/ref/spec#For_statements
https://github.com/golang/go/wiki/Range
https://blog.golang.org/strings