golang for語句完全指南
以下所有觀點都是個人愚見,有不同建議或補充的的歡迎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 省略表示式,等價於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
- 對於陣列、陣列指標或是分片值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 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的問題(詳細解答會在後邊)。
問題解答
-
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 := 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
相關文章
- Golang 獲取 goroutine id 完全指南Golang
- golang常用手冊:運算子、條件語句、迴圈語句Golang
- [譯] part 9: golang 迴圈語句Golang
- MySQL指南之SQL語句基礎MySql
- 【Golang 基礎系列十】Go 語言 條件語句之ifGolang
- systemctl 命令完全指南
- golang通過mysql語句實現分頁查詢GolangMySql
- golang透過mysql語句實現分頁查詢GolangMySql
- golang中一種不常見的switch語句寫法Golang
- flask之控制語句 if 語句與for語句Flask
- 精讀《useEffect 完全指南》
- MyBatis 完全使用指南MyBatis
- “Emacs 遊戲機”完全指南Mac遊戲
- 容器快速入門完全指南
- SpringData 完全入門指南Spring
- Linux systemctl 命令完全指南Linux
- 程式設計師的10句跟字面意思完全不同的常用語程式設計師
- 程式設計師的10句跟字面意思完全不同的常用語 你說過哪幾句?程式設計師
- seata-golang 接入指南Golang
- if語句
- 遊戲音訊不完全指南遊戲音訊
- 【VSC】Snippets不完全指南
- 【譯】ES10功能完全指南
- Spring event 使用完全指南Spring
- MySQL 不完全入門指南MySql
- PHP 檔案系統完全指南PHP
- 前端優化不完全指南前端優化
- webpack優化不完全指南Web優化
- 資料庫版本控制完全指南資料庫
- 免費商業模式完全指南模式
- Zsh 開發指南(第一篇 變數和語句)變數
- 【SQL】14 UNION 操作符、SELECT INTO 語句、INSERT INTO SELECT 語句、CREATE DATABASE 語句、CREATE TABLE 語句SQLDatabase
- Java學習之分支結構---判斷語句:if語句和switch語句Java
- Matlab的if語句switch語句for迴圈while迴圈語句練習MatlabWhile
- Golang指南:頂級Golang框架、IDE和工具列表Golang框架IDE
- MySQLDELETE語句MySqldelete
- JavaScript for in 語句JavaScript
- JavaScript 語句JavaScript