天天看點

Go語言學習14-基本流程控制基本流程控制

基本流程控制

Go語言在流程控制結構方面有些像C語言,但是在很多方面都與C不同。特點如下:

  • 在Go語言中沒有 do 和 while 循環,隻有一個更加廣義的 for 語句。
  • Go語言中的 switch 語句更加靈活多變。Go語言的 switch 語句還可以被用于進行類型判斷。
  • 與 for 語句類似,Go語言中的 if 語句和 switch 語句都可以接受一個可選的初始化子語句。
  • Go語言支援在 break 語句和 continue 語句之後跟一個可選的标記(Label)語句,以辨別需要終止或繼續的代碼塊。
  • Go語言中還有一個類似于多路轉接器的 select 語句。
  • Go語言中的go語句可以被用于靈活地啟用 Goroutine。
  • Go語言中的 defer 語句可以使我們更加友善地執行異常捕獲和資源回收任務。

1. 代碼塊和作用域

代碼塊就是一個由花括号 “{” 和 “}” 括起來的若幹表達式和語句的序列。代碼塊中也可以不包含任何内容,即為空代碼塊。

在Go語言的源代碼中,除了顯式的代碼塊之外,還有一些隐式的代碼塊,如下:

  • 所有Go語言源代碼形成了一個最大的代碼塊。這個最大的代碼塊也被稱為全域代碼塊。
  • 每一個代碼包都是一個代碼塊,即代碼包代碼塊。它們分别包含了目前代碼包内的所有Go語言源代碼。
  • 每一個源碼檔案都是一個代碼塊,即源碼檔案代碼塊。它們分别包含了目前檔案内的所有Go語言源碼。
  • 每一個 if 語句、for 語句、switch 語句和 select 語句都是一個代碼塊。
  • 每一個在 switch 或 select 語句中的子句都是一個代碼塊。

在Go語言中,每一個辨別符都有它的作用域。使用代碼塊表示詞法上的作用域範圍,規則如下:

  • 一個預定義辨別符的作用域是全域代碼塊。
  • 代表了一個常量、類型、變量或函數(不包括方法)的,被聲明在頂層的(即在任何函數聲明之外被聲明的)辨別符的作用域是代碼包代碼塊。
  • 一個被導入的代碼包的名稱的作用域是包含該代碼包導入語句的源碼檔案代碼塊。
  • 一個代表了方法接收者、方法參數或方法結果的辨別符的作用域是方法代碼塊。
  • 對于一個代表了常量或變量的辨別符,如果它被聲明在函數内部,那麼它的作用域總是包含它的聲明的那個最内層的代碼塊。
  • 對于一個代表了類型的辨別符,如果它被聲明在函數内部,那麼它的作用域就是包含它的聲明的那個最内層的代碼塊。

在Go語言中,可以在某個代碼塊中對一個已經在包含它的外層代碼塊中聲明過的辨別符進行重聲明。這種情況下,在外層代碼塊中聲明的那個同名辨別符被屏蔽了。例如:

package main

import (
    "fmt"
)

var v string = "1, 2, 3"

func main(){
    v := []int{1, 2, 3}
    if v != nil {  // 此時v代表的是一個切片類型值,是以可以與空值nil進行判等
        var v int = 123
        fmt.Printf("%v\n",v)
    }
}           

運作結果截圖如下:

Go語言學習14-基本流程控制基本流程控制

2. if 語句

Go語言的 if 語句總是以關鍵字 if 開始。之後,可以後跟一條簡單語句(當然也可以沒有),然後是一個作為條件判斷的布爾類型的表達式以及一個用花括号 “{” 和 “}” 括起來的代碼塊。if 語句也可以由 else 分支,它是 else 關鍵字和一個用花括号 “{” 和 “}” 括起來的代碼塊。

常用的簡單語句包括短變量聲明、指派語句和表達式語句。除了特殊的内建函數和代碼包 unsafe 中的函數,針對其他函數和方法的調用表達式和針對通道類型的接收表達式都可以出現在語句上下文中。在必要時,還可以使用圓括号将它們括起來。其他的簡單語句還包括發送語句、自增/自減語句和空語句。

Go語言 if 語句的舉例:

if 100 < number {
    number++
} else {
    number--
}           

在上面的 if 語句的條件表達式 100 < number 并沒有被圓括号括起來,這是Go語言的流程控制語句的特點之一。同時,強調一點是跟在條件表達式和 else 關鍵字之後的兩個代碼塊必須由花括号 “{” 和 “}” 括起來,不論代碼塊中包含幾條語句以及是否包含語句。

if 語句可以接受一條初始化子語句,常常用它來初始化一個變量如下:

if diff := 100 – number; 100 < diff { // 初始化子句和條件表達式之間是需要用分号“;”分隔的
    number++
} else if 200 < diff {
    number--
} else {
    number -= 2
}
           

由于在Go語言中一個函數可以傳回多個結果,是以常常會把在函數執行期間出現的正常錯誤也作為結果之一。例如,标準庫代碼包 os 中的函數 Open,它的聲明如下:

func Open(name string) (file *File, err error)           

函數 os.Open 傳回的第一個結果是與已經被“打開”的檔案相對應的 *File 類型的值,而第二個結果是代表了正常錯誤的 error 類型的值。error 是一個預定義的接口類型,所有實作它的類型都應該被用于描述一個正常錯誤。

在導入代碼包 os 之後,調用 Open 函數:

f, err := os.Open(name)
if err != nil {
    return err
}           

如上調用後,先檢查 err 的值是否為 nil。如果變量 err 的值不為 nil,那麼說明 os.Open 函數在被執行過程中發生了錯誤,這時變量 f 的的值肯定是不可用的。

在Go語言中,if 語句常被作為衛述語句。衛述語句是指被用來檢查關鍵的先決條件的合法性并在檢查未通過的情況下立即終止目前代碼塊的執行的語句。上面調用 Open 函數之後檢查的 if 語句就是衛述語句的一種。

func update (id int, department string) bool {
    if id <= 0 {
        return false
    }
    // 省略若幹語句
    return true
} // 在函數update開始處的那條if語句就屬于衛述語句。           

對函數稍加改造如下:

func update (id int, department string) error{ // 需要事先導入标準庫的代碼包errors
    if id <= 0 {
        return errors.New("The id is INVALID!")
    }
    // 省略若幹語句
    return nil
} // update函數傳回的結果不但可以表示在函數執行期間是否發生了錯誤,而且還可以展現出錯誤的具體描述。           

3. switch語句

語句 switch 可以使用表達式或者類型說明符作為 case 判定方法。switch 語句也就可以被分為兩類:表達式switch語句 和 類型switch語句。

3.1 表達式switch語句

在表達式 switch 語句中,switch 表達式和 case 攜帶的表達式(也稱為 case 表達式)都會被求值。對這些表達式的求值是自左向右、自上而下進行的。如果在 switch 語句中沒有顯示的switch 表達式,那麼 true 将會被作為 switch 表達式。例如:

switch content {
default:
    fmt.Println("Unknown language")
case "Python":
    fmt.Println("A interpreted language")
case "Go":
    fmt.Println("A compiled language")
}           

switch 關鍵字之後會緊跟一個 switch 表達式。switch 表達式中涉及的辨別符都必須是已經被聲明過的。同時還可以在 switch 和 switch 表達式之間插入一條簡單語句,如下:

switch content := getContent(); content { // content := getContent()會在switch表達式content被求值之前被執行
default:
    fmt.Println("Unknown language")
case "Python":
    fmt.Println("A interpreted language")
case "Go":
    fmt.Println("A compiled language")
}           

一條 case 語句由一個 case 表達式和一個語句清單組成,并且這兩者之間需要用冒号 : 分隔。一個 case 表達式由一個 case 關鍵字和一個表達式清單(可以包含多個表達式)組成。例如:

switch content := getContent(); content {
default:
    fmt.Println("Unknown language")
case "Python", "Ruby":
    fmt.Println("A interpreted language")
case "Go", "C", "Java":
    fmt.Println("A compiled language")
}           

在一條 case 語句中的語句清單的最後一條語句可以是 fallthrough 語句。一條 fallthrough 語句會将流程控制權轉移下一條 case 語句上。例如:

switch content := getContent(); content {
default:
    fmt.Println("Unknown language")
case "Ruby":
    fallthrough
case "Python":
    fmt.Println("A interpreted language")
case "Go", "C", "Java":
    fmt.Println("A compiled language")
}           

如上當變量 content 的值與 "Ruby" 相等的時候,在标準輸出上列印出的内容會是 A interpreted language。fallthrough 語句隻能夠作為 case 語句中的語句清單的最後一條語句, fallthrough 語句不能出現在最後一條 case 語句的語句清單中。

break 語句也可以出現在 case 語句清單中。一條 break 語句由一個 break 關鍵字和一個可選的标記組成,這兩者之間用空格分隔。例如:

switch content := getContent(); content {
default:
    fmt.Println("Unknown language")
case "Ruby":
    break
case "Python":
    fmt.Println("A interpreted language")
case "Go", "C", "Java":
    fmt.Println("A compiled language")
}           

3.2 類型switch語句

類型 switch 語句将對類型進行判定,而不是值。它的 switch 表達式的表現形式與類型斷言表達式相似,但與類型斷言表達式不同的是,它使用關鍵字 type 來充當欲判定的類型,而不是使用一個具體的類型字面量。例如:

switch v.(type){
    case string:
        fmt.Printf("The string is '%s'.\n", v.(string)) // v.(string)把v的值轉換成了string類型的值
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64: // v是byte類型或rune類型,也會執行下列分支
        fmt.Printf("The string is %d.\n", v)
    default:
        fmt.Printf("Unsupported value. (type=%T)\n", v)
    }
}           

在類型 switch 語句中,case 表達式中的類型字面量可以是 nil,如果 v 的值是 nil,那麼表達式 v.(type) 的結果值也會是 nil。與表達式 switch 語句不同的是,fallthrough 語句不允許出現在類型 switch 語句中。

對類型 switch 語句的 switch 表達式進行變形:

switch i:= v.(type){
    case string:
        fmt.Printf("The string is '%s'.\n", i)
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The integer is %d.\n", i)
    default:
        fmt.Printf("Unsupported value. (type=%T)\n", i)
    }
}           

第一個case語句相當于:

case string:
    i := v.(string)
    fmt.Printf("The string is '%s'.\n", i)           

對于包含多個類型字面量的 case 表達式,比如第二個 case 語句。例如,如果上面v的動态類型是 uint16 類型,那麼第二個 case 語句相當于:

case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
    i := v.(uint16)
    fmt.Printf("The integer is %d.\n", i)           

如上通過這種方式後,不需要在每個 case 語句中分别對那個欲判定類型的值進行顯示地類型轉換了。

在 switch 表達式缺失的情況下,該 switch 語句的判定目标會被視為布爾類型 true,也就是所有 case 表達式的結果值都應該是布爾類型。例如:

switch {
    case number < 100:
        number++
    case number < 200:
        number--
    default:
        number -= 2
    }
}           

同樣可以在 switch 關鍵字和 switch 表達式中添加一條簡單語句,例如:

switch number := 123; { // 這裡switch表達式缺失,預設switch的判定目标為布爾類型
    case number < 100:
        number++
    case number < 200:
        number--
    default:
        number -= 2
    }
}           

4. for 語句

4.1 for 子句

for 子句的3個部分是由固定順序組成,即初始化子句在左,條件在中,後置子句在右,并且在它們之間需要用分号“;”來分隔。可以在編寫 for 子句的時候省略掉其中的任何部分,為了避免歧義,與省略部分相鄰的分隔符“;”也必須被保留。初始化子句總會在充當條件的表達式被第一次求值之前執行,且隻會執行一次,而後置子句的執行總會在每次代碼塊執行完之後緊接着進行。後置子句一定不能是短變量聲明。例如:

for i := 0; i < 100; i++ {
    number++
}

var j uint = 1
for ; j%5 != 0; j *= 3 { // 省略初始化子句
    number++
}

for k != 1; k%5 != 0; { // 省略後置子句
    k *= 3
    number++
}           

在 for 子句的初始化子句和後置子句同時被省略或者其中的所有部分都被省略的情況下,分隔符 ; 可以被省略。例如:

// number是一個int類型的變量
for number < 200 {
    number += 2
}           

當 for 子句的3個部分都省略,就陷入了死循環。例如:

for {
    number++
}           

4.2 range 子句

for 語句使用 range 子句可以疊代出一個數組或切片值中的每個元素,一個字元串值中的每個字元或者一個字典值中的每個鍵值對,甚至可以被用于持續接收一個通道類型值中的元素。例如:

ints := []int{1, 2, 3, 4, 5}
for i, d := range ints {
    fmt.Printf("%d: %d\n", i, d)
}           

事先聲明了辨別符,例如:

var i, d int
ints := []int{1, 2, 3, 4, 5}
for i, d = range ints {
    fmt.Printf("%d: %d\n", i, d)
}           

運作截圖如下:

Go語言學習14-基本流程控制基本流程控制

4.3 range子句的疊代産出

range表達式的類型 第一個産出值 第二個産出值(若顯示擷取) 備注
a:[n]T、*[n]T或[]T i:int類型的元素索引值 與索引對應的元素的值a[i],類型為T a為range表達式的結果值,N為數組類型的長度,T為數組類型或切片類型的元素類型
s:string類型 與索引對應的元素的值s[i],類型為rune s為range表達式的結果值
m:map[K]T k:鍵值對中的鍵的值,類型為K 與鍵對應的元素值m[k],類型為T m為range表達式的結果值,K為字典類型的鍵的類型,V為字典類型的元素類型
c:chan T e: 元素的值,類型為T c為range表達式的結果值,T為通道類型的元素類型

對于所有可疊代的資料類型的值來說,可以要求每次疊代隻産出第一個疊代值。例如:

m := map[uint]string{1: "A", 6: "C", 7: "B"}
var maxKey uint
for k := range m{
    fmt.Printf("k: %d\n", k)
    if k > maxKey {
        maxKey = k
    }
}
fmt.Printf("maxKey: %d\n", maxKey)           
Go語言學習14-基本流程控制基本流程控制

忽略第一個疊代值而隻使用第二個疊代值的方法,如下:

m := map[uint]string{1: "A", 6: "C", 7: "B"}
var values []string
for _, v := range m {
    values = append(values, v)
}
fmt.Printf("values: %v\n", values)           
Go語言學習14-基本流程控制基本流程控制

在 for 語句中,可以使用 break 語句來終止 for 語句的執行。例如:

// 該變量的值包含了某個網絡的所有使用者昵稱及其重複次數
// 這個字典的鍵表示使用者昵稱,而值則代表了使用該昵稱的使用者數量。
var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "詩": 4}
// 存儲查找到所有的隻包含中文的使用者昵稱的計數資訊。
targetsCount := make(map[string]int)
for k, v := range namesCount {
    matched := true
    for _, r := range k {
        if r < '\u4e00' || r > '\u9fbf' { // 使用者昵稱中包含了非中文字元
            matched = false
            break // 隻會終止直接包含它的那條for語句的執行
        }
    }
    if matched {
        targetsCount[k] = v
    }
}
fmt.Printf("targetsCount: %v\n", targetsCount)           
Go語言學習14-基本流程控制基本流程控制

4.4 标記語句

一條标記語句可以成為 goto 語句、break 語句或 continue 語句的目标。标記語句中的标記隻是一個辨別符,它可以被放置在任何語句的左邊以作為這個語句的标簽。标記和被标記的語句之間需要用冒号來分隔。例如:

(1)break和标記語句的使用

var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "詩": 4}
    targetsCount := make(map[string]int)
L:
    for k, v := range namesCount {
        for _, r := range k {
            if r < '\u4e00' || r > '\u9fbf' {
                break L // 發現第一個非全中文的使用者昵稱的時候停止查找
            }
        }
        targetsCount[k] = v
    }
    fmt.Printf("targetsCount: %v\n", targetsCount)           
Go語言學習14-基本流程控制基本流程控制

在Go語言中 continue 語句隻能在 for 語句中被使用。continue 語句會使直接包含它的那個 for 循環直接進入下一次疊代。

(2)continue與标記語句的使用

var namesCount map[string]int = map[string]int{"霓虹": 3,"Huazie": 1, "Tom": 2, "詩": 4}
    targetsCount := make(map[string]int)
L:
    for k, v := range namesCount {
        for _, r := range k {
            if r < '\u4e00' || r > '\u9fbf' {
                continue L // L标記代表的那個for循環直接進入下一次疊代
            }
        }
        targetsCount[k] = v
    }
    fmt.Printf("targetsCount: %v\n", targetsCount)           
Go語言學習14-基本流程控制基本流程控制

使用Go語言的 for 語句寫出反轉一個切片類型值中的所有元素值的代碼。(不使用 在 for 語句之外聲明的任何變量作為輔助):

var numbers []int = []int{1,2,3,4,5}
fmt.Printf("before numbers: %v\n", numbers)
for i, j := 0, len(numbers)-1; i < j; i, j = i+1, j-1 {
    numbers[i], numbers[j] = numbers[j], numbers[i]
}
fmt.Printf("after numbers: %v\n", numbers)           
Go語言學習14-基本流程控制基本流程控制

初始化子句 和 後置子句 的隻能是單一語句而不能是多個語句,但是可以使用平行語句來豐富兩個子句的語義。

5. goto 語句

一條 goto 語句會把流程控制權無條件地轉移到它右邊的标記所代表的語句上。goto 語句隻能與标記語句連用,并且在它的右邊必須要出現一個标記。

goto 語句使用的注意點:

(1) 不允許因使用 goto 語句而使任何本不在目前作用域中的變量進入該作用域。例如:

goto L
    v := "B"
L:
    fmt.Printf("V: %v\n", v)           

這段代碼會造成一個編譯錯誤,主要原因是語句 goto L 恰恰使變量 v 的聲明語句被跳過了。

修改上面的代碼,保證順利通過編譯,如下:

v := "B"
    goto L
L:
    fmt.Printf("V: %v\n", v)           

把某條 goto 語句的直屬代碼塊叫作代碼塊 A,而把該條 goto 語句右邊的标記所指代的那條标記語句的直屬代碼塊叫作代碼塊 B,那麼隻要代碼塊 B 不是代碼塊 A 的外層代碼塊,這條 goto 語句就是不合法的。例如:

var n int = 10
if n % 3 != 0 {
    goto L1
}
switch {
case n % 7 == 0:
    fmt.Printf("%v is a common multiple of 7 and 3.\n", n)
default:
L1:
    fmt.Printf("%v isn't multiple of 3.\n", n)
}           

如上,标記 L1 所指代的标記語句的直屬代碼塊是由 switch 語句代表的,而 goto L1 語句的直屬代碼塊是由 if 語句代表的,并且前者并不是後者的直屬代碼塊。是以,goto L1 是非法的。

上面的代碼會出現編譯錯誤。修正上面的編譯錯誤,代碼如下:

var n int = 10
    if n % 3 != 0 {
        goto L1
    }
    switch {
    case n % 7 == 0:
        fmt.Printf("%v is a common multiple of 7 and 3.\n", n)
    default:
    }
L1:
    fmt.Printf("%v isn't multiple of 3.\n", n)           

利用 goto 語句跳出嵌套的流程控制語句的執行。例如:

// 查找name中的第一個非法字元并傳回
// 如果傳回的是空字元串說明name中不包含任何非法字元
func findEvildoer(name string) string{
    var evildoer string 
    for _, r := range name{
        switch {
        case r >= '\u0041' && r <= '\u005a': // a-z
        case r >= '\u0061' && r <= '\u007a': // A-Z
        case r >= '\u4e00' && r <= '\u9fbf': // 中文字元
        default:
            evildoer = string(r)
            goto L2
        }
    }
    goto L3
L2:
    fmt.Printf("The first evildoer of name '%s' is '%s'!\n", name, evildoer)
L3:
    return evildoer
}           

如下使用 break 和 if 語句替換上面的兩條 goto 語句:

func findEvildoer1(name string) string{
    var evildoer string 
L2:
    for _, r := range name{
        switch {
        case r >= '\u0041' && r <= '\u005a': // a-z
        case r >= '\u0061' && r <= '\u007a': // A-Z
        case r >= '\u4e00' && r <= '\u9fbf': // 中文字元
        default:
            evildoer = string(r)
            break L2
        }
    }
    if evildoer != "" {
        fmt.Printf("The first evildoer of name '%s' is '%s'!\n", name, evildoer)
    }
    return evildoer 
}           

(2) 另一個适合使用 goto 語句的場景是集中式的錯誤處理。例如:

func checkValidity(name string) error{
    var errDetail string    
    for i, r := range name{
        switch {
        case r >= '\u0041' && r <= '\u005a': // a-z
        case r >= '\u0061' && r <= '\u007a': // A-Z
        case r >= '\u4e00' && r <= '\u9fbf': // 中文字元
        case r == '_' || r == '-' || r == '.': // 其他允許的符号
        default:
            errDetail = "The name contains some illegal characters."
            goto L3
        }
        if i == 0 {
            switch r {
            case '_':
                errDetail = "The name can not begin with a '_'."
                goto L3
            case '-':
                errDetail = "The name can not begin with a '-'."
                goto L3
            case '.':
                errDetail = "The name can not begin with a '.'."
                goto L3
            }
        }
    }
    return nil
L3:
    return errors.New("Validity check failure: "+errDetail) 
}           

Go語言可以友善地從錯綜複雜的流程控制中跳出,但是 goto 語句的代碼塊的可讀性大大下降。是以,要節制地使用 goto 語句。

結語

繼續閱讀