Go語言基礎05-Go流程控制
www.leexide.com
希望每一位尋求轉載的朋友都能夠按照要求進行,鼓勵原創,尊重原創。
微信公眾號:DevOps運維運營之家
QQ號碼:1045884038
E-mail:[email protected]
如有問題或建議,請關註微信公眾號
1 概述
流程控制是順序編程中必不可少的一部分,它是整個編程基礎的重要一環。在順序編程的流程控制部分,Go語言和其他主流語言有一些差別,主要體現在Go語言沒有do-while
語句,因此for
語句擁有更廣泛的含義與用途。另一方面switch
語句也有一些擴展,例如支持類型判斷和初始化子語句等。
除了這些常見的流程控制語法的關鍵字,Go語言還有三個特殊的關鍵字,分別是:
defer
:用於捕獲異常和資源回收等工作;select
:用於多分支選擇(配合通道使用);go
:用於異步啟動goroutine並執行特定函數。
2 條件語句
2.1 if判斷
例子:
package main
import "fmt"
func main() {
a := 1
if a < 20 {
fmt.Printf("a小於20\n")
}
fmt.Printf("a的值是:%d\n", a)
}
2.2 if-else判斷
例子:
package main import "fmt" func main() { a := 100 if a < 20 { fmt.Printf("a小於20\n") } else { fmt.Printf("a大於20\n") } fmt.Printf("a的值是:%d\n", a) }
例子:
package main import "fmt" func main() { a := 100 if a < 20 { fmt.Printf("a小於20\n") if a > 10 { fmt.Printf("a大於10\n") } else { fmt.Printf("a小於10\n") } } else { fmt.Printf("a大於20\n") } fmt.Printf("a的值是:%d\n", a) } }
說明:
上面的代碼嵌套了一層if-else
語句,這在編程規範中可以說大忌,特別是在邏輯復雜的情況下,嵌套if語句將非常影響性能,因此就有了else-if
語句。
2.3 else-if判斷
else-if
語句是在前面if-else
語句之上再度擴展的,為了解決多重判斷的問題。例如:
package main
import "fmt"
func main() {
a := 11
if a > 20 {
fmt.Printf("a大於20\n")
} else if a < 10 {
fmt.Printf("a小於10\n")
} else {
fmt.Printf("a大於10\n")
fmt.Printf("a小於20\n")
}
fmt.Printf("a的值是:%d\n", a)
}
else-if
語句可以連續使用多個else if
關鍵字,例如判斷一個數字是否大於10小於20且不等於11:
package main
import "fmt"
func main() {
a := 13
if a > 20 {
fmt.Printf("a大於20\n")
} else if a < 10 {
fmt.Printf("a小於10\n")
} else if a == 11 {
fmt.Printf("a等於11\n")
} else {
fmt.Printf("a大於10\n")
fmt.Printf("a小於20\n")
fmt.Printf("a不等於11\n")
}
fmt.Printf("a的值是:%d\n", a)
}
2.4 初始化子語句
if語句可以有一個子語句,用於初始化局部變量,例如:
package main
import "fmt"
func main() {
if a := 10; a < 20 {
fmt.Printf("a小於20\n")
} else {
fmt.Printf("a的值是:%d\n", a)
}
}
註意1:
子語句只能有一個表達式,比如下面的例子是無法編譯的。
func main() {
if b := 10; a := 10; a < 20 { //編譯出錯,初始化子語句只能有一個表達式
fmt.Printf("a小於20\n")
} else {
fmt.Printf("a的值是:%d\n", a)
}
}
註意2:
a的值是在if代碼塊中定義的,所以不能在代碼塊之外調用,例如下面代碼也是無法編譯的:
func main() {
if a := 10; a < 20 {
fmt.Printf("a小於20\n")
}
//編譯出錯,a是在if代碼塊中定義的,所以不能再函數中調用
fmt.Printf("a的值是:%d\n", a)
}
3 選擇語句
在上面使用if條件語句時,一般用於二元判斷,對於多元判斷使用條件語句就會顯得煩瑣,所以就有了選擇語句。例如判斷一個數字屬於哪個區間就可以用選擇語句輕松實現,避免條件語句煩瑣的嵌套過程。
3.1 switch語句
在Go語言中,switch
表示選擇語句的關鍵字,switch
語句會根據初始化表達式得出一個值,然後根據case
語句的條件,執行相應的代碼塊,最終返回特定內容。每個case
被稱為一種情況,只有當初始化語句的值符合case
的條件語句時,case
才會被執行。
如果沒有遇到符合的case
,則可以使用默認的case
(default case
),如果己經遇到了
符合的case
,那麽後面的case
都不會被執行。
與其他編程語言不同的是,在Go語言編程中,switch
有兩種類型。
- 表達式
switch
:在表達式switch
中,case
包含與switch
表達式的值進行比較的表達式。 - 類型
switch
:在類型switch
中,case
包含與特殊註釋的switch
表達式的類型進行比較的類型。
3.1.1 表達式switch
例子:
package main
import "fmt"
func main() {
grade := "B"
marks := 90
switch marks {
case 90:
grade = "A"
case 80:
grade = "B"
case 60, 70:
grade = "C"
default:
grade = "D"
}
fmt.Printf("你的成績為%s\n", grade)
}
這種方式雖然簡單,但一般不這樣寫,因為不容易擴展這個選擇語句,例如把分數改為100或者91時,也會返回“你的成績為D”,這明顯不符合實際情況。這是因為選擇語句中沒有考慮值的範圍的問題,現在假設90分到100分為A, 80分到89分為B, 60分到79分為C ,低於60分則為D 。這種情況下上面的單值判斷己經不能滿足我們此時的需求。
因此就需要完整的switch 表達式寫法了:
package main
import "fmt"
func main() {
grade := "E"
marks := 90
switch {
case marks >= 90:
grade = "A"
case marks >= 80:
grade = "B"
case marks >= 70:
grade = "C"
case marks >= 60:
grade = "D"
default:
grade = "E"
}
switch {
case grade == "A":
fmt.Printf("你的成績優秀!\n")
case grade == "B":
fmt.Printf("表現良好!\n")
case grade == "C", grade == "D": //case表達式可以有多個
fmt.Printf("再接再厲!\n")
default:
fmt.Printf("成績不合格!\n")
}
fmt.Printf("你的成績為%s\n", grade)
}
上面例子中有兩個switch
相互關聯,第一個switch
語句判斷成績所屬區間,並得出grade
的值, 然後第二個switch
根據grade
的值返回相應的話語。
註意,每一個case
都可以擁有多個表達式,它的含義與fallthrough
一樣,例如下面兩種寫法是一樣的意思:
/* 多表達式寫法 */
case grade == "C", grade == "D": //case表達式可以有多個
fmt.Printf("再接再厲!\n")
/* fallthrough寫法 */
case grade == "C":
fallthrough
case grade == "D":
fmt.Printf("再接再厲!\n")
fallthrough
關鍵詞可以把當前case
控制權交給下一個case
語句判斷。
3.1.2 類型switch
類型switch
語句針對變量類型判斷執行哪個case
代碼塊,下面是一個簡單的例子:
package main
import "fmt"
var x interface{} //空接口
func main() {
x = 1
switch i := x.(type) { //這裏表達式只有一句初始化子語句
case nil:
fmt.Printf("這裏是nil,x的類型是%T", i)
case int:
fmt.Printf("這裏是int,x的類型是%T", i)
case float64:
fmt.Printf("這裏是float64,x的類型是%T", i)
case bool:
fmt.Printf("這裏是bool,x的類型是%T", i)
case string:
fmt.Printf("這裏是string,x的類型是%T", i)
default:
fmt.Printf("未知類型")
}
}
類型switch
的初始化子語句中需要判斷的變量必須是具有接口類型的變量(如果是固定類型的變量就沒有判斷的意義了)。在語法上類型switch
與表達式switch
沒有太大區別。
3.2 switch初始化語句
switch
語句同樣擁有初始化子語句,和if
一樣均是寫在關鍵字後面,只能有一句語句,例如:
/* 單值判斷寫法 */
switch marks := 90; marks {
case 90:
grade = "A"
case 80:
grade = "B"
case 70:
grade = "C"
case 60:
grade = "D"
default:
grade = "E"
}
/* 範圍表達式寫法 */
switch marks := 90; { //這裏的分號不能省略
case marks >= 90:
grade = "A"
case marks >= 80:
grade = "B"
case marks >= 70:
grade = "C"
case marks >= 60:
grade = "D"
default:
grade = "E"
}
3.3 select語句
在Go語言中,除了switch
語句,還有一種選擇語句一-select
,這種選擇語句用於配合通道(channel
)的讀寫操作,用於多個channel
的並發讀寫操作。
switch
是按順序從上到下依次執行的,而select
是隨機選擇一個case
來判斷,直到匹配其中的一個case
,舉個例子:
package main
import "fmt"
func main() {
a := make(chan int, 1024)
b := make(chan int, 1024)
for i := 0; i < 10; i++ {
fmt.Printf("第%d次", i)
a <- 1
b <- 1
select {
case <-a:
fmt.Println("from a")
case <-b:
fmt.Println("from b")
}
}
}
上述代碼的意思是,同時在a
和b
中選擇,哪個有內容就從哪個讀,由於channel
的讀寫操作是阻塞操作,使用select
語句可以避免單個channel
的阻塞。此外select
同樣可以使用default
代碼塊,用於避免所有channel
同時阻塞。
4 循環語句
循環語句是編程中常使用的流程控制語句之一,在Go語言中,循環語句的關鍵字是for
,沒有while
關鍵字。for
語句可以根據指定的條件重復執行其內部的代碼塊,這個判斷條件一般是由for
關鍵字後面的子語句給出的。
例如一個簡單的for
循環例子:
package main
import "fmt"
func main() {
for a := 0; a < 5; a++ {
fmt.Printf("a的值是:%d\n", a)
}
}
上面for
關鍵字後面有三個子語句,初始化變量a
為0
,並判斷當a
小於5
時執行下面代碼塊的內容,每次判斷a
的值都加l
,直到不符合初始化語句的判斷條件,進而退出循環。
4.1 for的子語句
for
語句後面的三個子語句我們稱為:
- 初始化子語句
- 條件子語句
- 後置子語句
這三者不能顛倒順序,其中條件子語句是必需的,條件子語句會返回一個布爾型,true
則執行代碼塊,false
則跳出循環。
例如以下示例代碼中就省略了初始化子語句和後置子語句:
package main
import "fmt"
func main() {
a := 0
b := 5
for a < b {
a++
fmt.Printf("a的值是:%d\n", a)
}
}
在上面的例子中,for
關鍵字後面只有一個a< b
的判斷語句,這是典型的條件判斷語
句,它實際上是三個子語句的簡寫:
for ; a < b ;
//Go語言編譯器會自動判斷三個子語句中是否存在條件子語句
//例如寫成這樣就會報錯
for ; ; a < b
後置子語句的意思就是先進行條件子語句判斷,for
代碼塊執行之後再對條件變量操作的語句進行判斷,上面例子中的a++
就是一個後置子語句。
4.2 range子語句
每一個for
語句都可以使用一個特殊的range
子語句,其作用類似於叠代器,用於輪詢數組或者切片值中的每一個元素,也可以用於輪詢字符串的每一個字符,以及字典值中的每個鍵值對,甚至還可以持續讀取一個通道類型值中的元素。
例子:
package main
import "fmt"
func main() {
str := "abcz"
for i, char := range str {
fmt.Printf("字符串第%d個字符的值為%d\n", i, char)
}
for _, char := range str { //忽略第一個值(忽略index)
println(char)
}
for i := range str { //忽略第二個值
fmt.Println(i)
}
for range str { //忽略全部返回值,只執行下面代碼塊
println("執行成功")
}
}
range
關鍵宇右邊是range
表達式,表達式一般寫在for
語句前面,以便提高代碼易讀性。像上面的例子可以寫成:
for i := range "abcz"
//把原來的str := "abcz"表達式寫到range關鍵字後面
這樣不僅降低了可讀性,也不容易管理後續的循環代碼塊。
range
關鍵宇左邊表示的是一對索引-值對,根據不同的表達式返回不同的結果,詳見下表。
右邊表達式返回的類型 | 第一個值 | 第二個值 |
---|---|---|
string | index | str[index],返回類型為rune |
array/slice | index | str[index] |
map | key | m[key] |
channel | element |
從上表可以看出,除了輪詢字符串,還支持其他類型,例如數組,切片,字典甚至通道等等。
例子:
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
println(k, v)
}
numbers := []int{1, 2, 3, 4}
for i, x := range numbers {
fmt.Printf("第%d次,x的值為%d。\n", i, x)
}
}
返回第一個值為索引值(鍵值),有時候並不是我們所需要的,因此可以使用“_
”空標識符表示忽略第一個返回值。對於空字典或切片、空數組、空字符串等情況,for
語句會直接結束,不會循環。
但如果需要指定for
執行循環的次數,例如需要獲取數組(或者其他支持的類型)裏面的值,而數組中有一個值是空字符串,則可以指定數組(或其他支持類型)長度,強制讓for
循環執行相應次數,例如將上面的例子稍微改一下:
package main
import "fmt"
func main() {
numbers := [5]int{1, 2, 3, 4}
for i, x := range numbers {
fmt.Printf("第%d次,x的值為%d。\n", i, x)
}
}
由於定義了numbers
長度為5
,但numbers
中只有4
個值,因此最後一個為空值,從for
循環返回的信息可以看到第5
次x
的值為0
,代碼塊的確執行了5
次。
5 延遲語句
defer
用於延遲調用指定函數,defer
關鍵字只能出現在函數內部。
例子:
package main
import "fmt"
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
上面例子會首先打印Hello
,然後再打印World
,因為第一句使用了defer
關鍵字, defer
語句會在函數最後執行,被延遲的操作是defer
後面的內容。
defer
後面的表達式必須是外部函數的調用,上面的例子就是針對fmt.Println
函數的延遲調用。defer
有兩大特點:
- 只有當
defer
語句全部執行,defer
所在函數才算真正結束執行。 - 當函數中有
defer
語句時,需要等待所有defer
語句執行完畢,才會執行return
語句。
因為defer
的延遲特點,可以把defer
語句用於回收資源、清理收尾等工作。使用defer
語句之後,不用糾結回收代碼放在哪裏,反正都是最後執行。
這裏需要註意defer
的執行時機,例如下面的例子:
package main
import "fmt"
var i = 0
func print() {
fmt.Println(i)
}
func main() {
for ; i < 5; i++ {
defer print()
}
}
上面例子返回的是5
個5
,這是因為每個defer
都是在函數輪詢之後,最後才執行,此時i
的值當然就是5
了。如果要正確反向打印數字則應該這樣寫:
package main
import "fmt"
var i = 0
func print(i int) {
fmt.Println(i)
}
func main() {
for ; i < 5; i++ {
defer print(i)
}
}
上面例子引入了函數參數,雖然還沒介紹函數的概念,但是不妨礙理解這裏面的defer
關鍵知識。這裏之所以是一個反序的數字列表,是因為defer
其實是一個棧,遵循先入後出,或者理解為後進先出。
當i
等於0 時,defer
語句第一次被壓棧,此時defer
後面的函數返回0
; i
不斷自增,一直到i
等於4
時,defer
語句第5
次入棧,defer
後的函數返回4
;此時i
的自增不再滿足for
條件,於是跳出循環,在結束之前,Go語言會根據defer
後進先出原則逐條打印棧內的數值,於是就出現現在看到的結果了。
簡單來說就是當一個函數內部有多個defer
語句時,最後面的defer
語句最先執行(當然是指在所有defer
語句中) 。
6 標簽
在Go語言中,有一個特殊的概念就是標簽,可以給for
、switch
或select
等流程控制代碼塊打上一個標簽,配合標簽標識符可以方便跳轉到某一個地方繼續執行,有助於提高編程效率。標簽格式如下:
L1:
for i := 0; i <= 5; i++ {
//代碼塊
}
//下面寫法也可以,不過Go語言編譯器會自動格式化為上面的格式
L2:switch i {
//代碼塊
}