1. 程式人生 > >Go語言基礎05-Go流程控制

Go語言基礎05-Go流程控制

兩個 示例代碼 goroutine 空字符 不同的 文章 優秀 als 支持

文章轉載請註明出處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")
        }
    }
}

上述代碼的意思是,同時在ab中選擇,哪個有內容就從哪個讀,由於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關鍵字後面有三個子語句,初始化變量a0,並判斷當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&lt; 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循環返回的信息可以看到第5x的值為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()
    }
}

上面例子返回的是55,這是因為每個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語言中,有一個特殊的概念就是標簽,可以給forswitchselect等流程控制代碼塊打上一個標簽,配合標簽標識符可以方便跳轉到某一個地方繼續執行,有助於提高編程效率。標簽格式如下:

L1:
    for i := 0; i <= 5; i++ {
        //代碼塊
    }

//下面寫法也可以,不過Go語言編譯器會自動格式化為上面的格式
L2:switch i {
    //代碼塊
}