golang for語句完全指南

sheepbao發表於2018-01-15

以下所有觀點都是個人愚見,有不同建議或補充的的歡迎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 省略表示式,等價於true
for {   // for true {
        f(doThing)
}

這種格式,只有單個邏輯表示式, 邏輯表示式的值為true,則繼續執行,否則停止迴圈。

for語句中兩個分號

形式:

for i:=0; i < 10; i++ {
        f(doThing)
}
// or
for i:=0; i < 10; {
        i++
        f(doThing)
}
// or 
var i int
for ; 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)
}
// or
for range []int{1,2,3} {
    f(doThing)
}

用range來迭代資料是最常用的一種for語句,range右邊的表示式叫範圍表示式, 範圍表示式可以是陣列,陣列指標,slice,字串,map和channel。因為要賦值, 所以左側的運算元(也就是迭代變數)必須要可定址的,或者是map下標的表示式。 如果迭代變數是一個channel,那麼只允許一個迭代變數,除此之外迭代變數可以有一個或者兩個。

範圍表示式在開始迴圈之前只進行一次求值,只有一個例外:如果範圍表示式是陣列或指向陣列的指標, 至多有一個迭代變數存在,只對範圍表示式的長度進行求值;如果長度為常數,範圍表示式本身將不被求值。

每迭代一次,左邊的函式呼叫求值。對於每個迭代,如果相應的迭代變數存在,則迭代值如下所示生成:

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
  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 6
    f(i)
}

var a [10]string
for i, s := range a {
    // type of i is int
    // type of s is string
    // s == a[i]
    g(i, s)
}

var key string
var val interface {}  // value type of m is assignable to val
m := 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 channel
for 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 := array
for 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 := false
    for 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 := false
    for 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

相關文章