golang for語句完全指南

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

以下所有觀點都是個人愚見,有不同建議或補充的的歡迎emialaboutme
原文章地址

關於for語句的疑問
for語句的規範
for語句的內部實現-array
問題解答

關於for語句的疑問

我們都知道在golang中,迴圈語句只有for這一個,在代碼中寫一個迴圈都一般都需要用到for(當然你用goto也是可以的),雖然golang的for語句很方便,但不少初學者一樣對for語句持有不少疑問,如:

  1. for語句一共有多少種運算式格式?
  2. for語句中臨時變數是怎麼回事?(為什麼有時遍曆賦值後,所有的值都等於最後一個元素)
  3. range後面支援的資料類型有哪些?
  4. range string類型為何得到的是rune類型?
  5. 遍曆slice的時候增加或刪除資料會怎麼樣?
  6. 遍曆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
  1. 對於數組、數組指標或是分區值a來說,下標迭代值升序產生,從0開始。有一種特殊情境,只有一個迭代參數存在的情況下,range迴圈產生0到len(a)的迭代值,而不是索引到數組或是分區。對於一個nil分區,迭代的數量為0。
  2. 對於字串類型,range子句迭代字串中每一個Unicode代碼點,從下標0開始。在連續迭代中,下標值會是下一個utf-8代碼點的第一個位元組的下標,而第二個實值型別是rune,會是對應的代碼點。如果迭代遇到了一個非法的Unicode序列,那麼第二個值是0xFFFD,也就是Unicode的替換字元,然後下一次迭代只會前進一個位元組。
  3. map中的迭代順序是沒有指定的,也不保證兩次迭代是一樣的。如果map元素在迭代過程中被刪掉了,那麼對應的迭代值不會再產生。如果map元素在迭代中插入了,則該元素可能在迭代過程中產生,也可能被跳過,但是每個元素的迭代值頂多出現一次。如果map是nil,那麼迭代次數為0。
  4. 對於管道,迭代值就是下一個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的問題(詳細解答會在後邊)。

問題解答

  1. for語句一共有多少種運算式格式?
    這個問題應該很簡單了,上面的規範中就有答案了,一共有3種:

    Condition = Expression .ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
  2. 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。

  3. range後面支援的資料類型有哪些?
    共5個,分別是數組,數組指標,slice,字串,map和channel

  4. range string類型為何得到的是rune類型?
    這個問題在for規範中也有解答,對於字串類型,在連續迭代中,下標值會是下一個utf-8代碼點的第一個位元組的下標,而第二個實值型別是rune。如果迭代遇到了一個非法的Unicode序列,那麼第二個值是0xFFFD,也就是Unicode的替換字元,然後下一次迭代只會前進一個位元組。

    其實看完這句話,我沒理解,當然這句話告訴我們了遍曆string得到的第二個實值型別是rune,但是為什麼是rune類型,而不是string或者其他類型?後來在看了Rob Pike寫的blogStrings, bytes, runes and characters in Go才明白點,首先需要知道runeint32的別名,且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了。

  5. 遍曆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
  6. 遍曆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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.