go 流程控制之switch 語句介紹

賈維斯Echo發表於2023-10-12

go 流程控制之switch 語句介紹

一、switch語句介紹

1.1 認識 switch 語句

我們先透過一個例子來直觀地感受一下 switch 語句的優點。在一些執行分支較多的場景下,使用 switch 分支控制語句可以讓程式碼更簡潔,可讀性更好。

比如下面例子中的 readByExt 函式會根據傳入的副檔名輸出不同的日誌,使用 if 語句進行分支控制:

func readByExt(ext string) {
    if ext == "json" {
        println("read json file")
    } else if ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "gif" {
        println("read image file")
    } else if ext == "txt" || ext == "md" {
        println("read text file")
    } else if ext == "yml" || ext == "yaml" {
        println("read yaml file")
    } else if ext == "ini" {
        println("read ini file")
    } else {
        println("unsupported file extension:", ext)
    }
}

如果用 switch 改寫上述例子程式碼,我們可以這樣來寫:

func readByExtBySwitch(ext string) {
    switch ext {
    case "json":
        println("read json file")
    case "jpg", "jpeg", "png", "gif":
        println("read image file")
    case "txt", "md":
        println("read text file")
    case "yml", "yaml":
        println("read yaml file")
    case "ini":
        println("read ini file")
    default:
        println("unsupported file extension:", ext)
    }
}

從程式碼呈現的角度來看,針對這個例子,使用 switch 語句的實現要比 if 語句的實現更加簡潔緊湊。

簡單來說,readByExtBySwitch 函式就是將輸入引數 ext 與每個 case 語句後面的表示式做比較,如果相等,就執行這個 case 語句後面的分支,然後函式返回。

1.2 基本語法

在Go程式語言中,switch語句的基本語法如下:

switch initStmt; expr {
    case expr1:
        // 執行分支1
    case expr2:
        // 執行分支2
    case expr3_1, expr3_2, expr3_3:
        // 執行分支3
    case expr4:
        // 執行分支4
    ... ...
    case exprN:
        // 執行分支N
    default: 
        // 執行預設分支
}

我們按語句順序來分析一下:

  • 首先 switch 語句第一行由 switch 關鍵字開始,它的後面通常接著一個表示式(expr),這句中的 initStmt 是一個可選的組成部分。和 iffor 語句一樣,我們可以在 initStmt 中透過短變數宣告定義一些在 switch 語句中使用的臨時變數。
  • 接下來,switch 後面的大括號內是一個個程式碼執行分支,每個分支以 case 關鍵字開始,每個 case 後面是一個表示式或是一個逗號分隔的表示式列表。
  • 最後,還有一個以 default 關鍵字開始的特殊分支,被稱為預設分支default 子句是可選的,如果沒有一個case子句匹配expression的值,將執行default子句中的程式碼塊。

最後,我們再來看 switch 語句的執行流程:

  • 首先,switch 語句會用 expr 的求值結果與各個 case 中的表示式結果進行比較,如果發現匹配的 case,也就是 case 後面的表示式,或者表示式列表中任意一個表示式的求值結果與 expr 的求值結果相同,那麼就會執行該 case 對應的程式碼分支,分支執行後,switch 語句也就結束了。
  • 如果所有 case 表示式都無法與 expr 匹配,那麼程式就會執行 default 預設分支,並且結束 switch 語句。

二、Go語言switch語句中case表示式求值順序

2.1 switch語句中case表示式求值次序介紹

接下來,我們再來看看,在有多個 case 執行分支的 switch 語句中,Go 是按什麼次序對各個 case 表示式進行求值,並且與 switch 表示式(expr)進行比較的?

我們先來看一段示例程式碼,這是一個一般形式的 switch 語句,為了能呈現 switch 語句的執行次序,以多個輸出特定日誌的函式作為 switch 表示式以及各個 case 表示式:

func case1() int {
    println("eval case1 expr")
    return 1
}

func case2_1() int {
    println("eval case2_1 expr")
    return 0 
}
func case2_2() int {
    println("eval case2_2 expr")
    return 2 
}

func case3() int {
    println("eval case3 expr")
    return 3
}

func switchexpr() int {
    println("eval switch expr")
    return 2
}

func main() {
    switch switchexpr() {
    case case1():
        println("exec case1")
    case case2_1(), case2_2():
        println("exec case2")
    case case3():
        println("exec case3")
    default:
        println("exec default")
    }
}

執行一下這個示例程式,我們得到如下結果:

eval switch expr
eval case1 expr
eval case2_1 expr
eval case2_2 expr
exec case2

從輸出結果中我們看到,Go 先對 switch expr 表示式進行求值,然後再按 case 語句的出現順序,從上到下進行逐一求值。在帶有表示式列表的 case 語句中,Go 會從左到右,對列表中的表示式進行求值,比如示例中的 case2_1 函式就執行於 case2_2 函式之前。

如果 switch 表示式匹配到了某個 case 表示式,那麼程式就會執行這個 case 對應的程式碼分支,比如示例中的“exec case2”。這個分支後面的 case 表示式將不會再得到求值機會,比如示例不會執行 case3 函式。這裡要注意一點,即便後面的 case 表示式求值後也能與 switch 表示式匹配上,Go 也不會繼續去對這些表示式進行求值了,這是switch語句的工作原理。

除了這一點外,你還要注意 default 分支。無論 default 分支出現在什麼位置,它都只會在所有 case 都沒有匹配上的情況下才會被執行的

不知道你有沒有發現,這裡其實有一個最佳化小技巧,考慮到 switch 語句是按照 case 出現的先後順序對 case 表示式進行求值的,那麼如果我們將匹配成功機率高的 case 表示式排在前面,就會有助於提升 switch 語句執行效率。這點對於 case 後面是表示式列表的語句同樣有效,我們可以將匹配機率最高的表示式放在表示式列表的最左側。

2.2 switch語句中case表示式的求值次序特點

Go語言switch語句中case表示式的求值次序特點:

  1. switch語句首先求值switch表示式,然後按case出現順序逐一求值case表示式。
  2. 一旦某個case表示式匹配成功後,就執行對應的程式碼塊,之後case不再求值。
  3. 即使後續的case表示式匹配成功,也不會再求值。
  4. 所有case都不匹配的情況下,會執行預設的default案例。
  5. default位置靈活,可以放在開頭或結尾。
  6. case後帶表示式列表時,會從左到右求值列表中的表示式。
  7. 將匹配機率高的case排在前面,可以最佳化執行效率。

三、switch 語句的靈活性

3.1 switch 語句各表示式的求值結果支援各種型別值

首先,switch 語句各表示式的求值結果可以為各種型別值,只要它的型別支援比較操作就可以了。

Go 語言只要型別支援比較操作,都可以作為 switch 語句中的表示式型別。比如整型、布林型別、字串型別、複數型別、元素型別都是可比較型別的陣列型別,甚至欄位型別都是可比較型別的結構體型別也可以。下面就是一個使用自定義結構體型別作為switch表示式型別的例子:

type person struct {
    name string
    age  int
}

func main() {
    p := person{"tom", 13}
    switch p {
    case person{"tony", 33}:
        println("match tony")
    case person{"tom", 13}:
        println("match tom")
    case person{"lucy", 23}:
        println("match lucy")
    default:
        println("no match")
    }
}

實際開發過程中,以結構體型別為 switch表示式型別的情況並不常見,這裡舉這個例子僅是為了說明 Go switch 語句對各種型別支援的廣泛性。

而且,當 switch 表示式的型別為布林型別時,如果求值結果始終為 true,那麼我們甚至可以省略 switch 後面的表示式,比如下面例子:

// 帶有initStmt語句的switch語句
switch initStmt; {
    case bool_expr1:
    case bool_expr2:
    ... ...
}

// 沒有initStmt語句的switch語句
switch {
    case bool_expr1:
    case bool_expr2:
    ... ...
}

注意:在帶有 initStmt 的情況下,如果我們省略 switch 表示式,那麼 initStmt 後面的分號不能省略,因為 initStmt 是一個語句。

3.2 switch 語句支援宣告臨時變數

在前面介紹 switch 語句的一般形式中,我們看到,和 if、for 等控制結構語句一樣,switch 語句的 initStmt 可用來宣告只在這個 switch 隱式程式碼塊中使用的變數,這種就近宣告的變數最大程度地縮小了變數的作用域。

示例:

switch x := someFunction(); x {
case 1:
    fmt.Println("x is 1")
case 2:
    fmt.Println("x is 2")
default:
    fmt.Println("x is something else")
}

// 這裡無法訪問 x,因為它的作用域僅限於 switch 語句

在上面的示例中,x是一個區域性變數,只在switch語句內部可見。這可以有效地限制變數的生存期和可見性,從而提高程式碼的清晰度和健壯性。這是Go語言在控制結構中的一種好實踐。

3.3 case 語句支援表示式列表

在Go的switch語句中,case語句支援表示式列表,一個分支可以有多個值,多個case值中間使用英文逗號分隔。這意味著你可以在一個case子句中列出多個表示式,以匹配其中任何一個表示式。如果switch表示式的值與列表中的任何一個表示式匹配,相應的case分支將被執行。

func checkWorkday(a int) {
    switch a {
    case 1, 2, 3, 4, 5:
        println("it is a work day")
    case 6, 7:
        println("it is a weekend day")
    default:
        println("are you live on earth")
    }
}

3.4 取消了預設執行下一個 case 程式碼邏輯的語義

在 C 語言中,如果匹配到的 case 對應的程式碼分支中沒有顯式呼叫 break 語句,那麼程式碼將繼續執行下一個 case 的程式碼分支,這種“隱式語義”並不符合日常演演算法的常規邏輯,這也經常被詬病為 C 語言的一個缺陷。要修復這個缺陷,我們只能在每個 case 執行語句中都顯式呼叫 break。

Go 語言中的 Swith 語句就修復了 C 語言的這個缺陷,取消了預設執行下一個 case 程式碼邏輯的“非常規”語義,每個 case 對應的分支程式碼執行完後就結束 switch 語句。

如果在少數場景下,你需要執行下一個 case 的程式碼邏輯,你可以顯式使用 Go 提供的關鍵字 fallthrough 來實現,fallthrough語法可以執行滿足條件的case的下一個case,是為了相容C語言中的case設計的。下面就是一個使用 fallthrough 的 switch 語句的例子,我們簡單來看一下:

func case1() int {
    println("eval case1 expr")
    return 1
}

func case2() int {
    println("eval case2 expr")
    return 2
}

func switchexpr() int {
    println("eval switch expr")
    return 1
}

func main() {
    switch switchexpr() {
    case case1():
        println("exec case1")
        fallthrough
    case case2():
        println("exec case2")
        fallthrough
    default:
        println("exec default")
    }
}

執行一下這個示例程式,我們得到這樣的結果:

eval switch expr
eval case1 expr
exec case1
exec case2
exec default

我們看到,switch expr 的求值結果與 case1 匹配成功,Go 執行了 case1 對應的程式碼分支。而且,由於 case1 程式碼分支中顯式使用了 fallthrough,執行完 case1 後,程式碼執行流並沒有離開 switch 語句,而是繼續執行下一個 case,也就是 case2 的程式碼分支。

這裡有一個注意點,由於 fallthrough 的存在,Go 不會對 case2 的表示式做求值操作,而會直接執行 case2 對應的程式碼分支。而且,在這裡 case2 中的程式碼分支也顯式使用了 fallthrough,於是最後一個程式碼分支,也就是 default 分支對應的程式碼也被執行了。

另外,還有一點要注意的是,如果某個 case 語句已經是 switch 語句中的最後一個 case 了,並且它的後面也沒有 default 分支了,那麼這個 case 中就不能再使用 fallthrough,否則編譯器就會報錯。

到這裡,我們看到 Go 的 switch 語句不僅修復了 C 語言 switch 的缺陷,還為 Go 開發人員提供了更大的靈活性,我們可以使用更多型別表示式作為 switch 表示式型別,也可以使用 case 表示式列表簡化實現邏輯,還可以自行根據需要,確定是否使用 fallthrough 關鍵字繼續向下執行下一個 case 的程式碼分支。

四、type switch

“type switch”這是一種特殊的 switch 語句用法,我們透過一個例子來看一下它具體的使用形式:

func main() {
    var x interface{} = 13
    switch x.(type) {
    case nil:
        println("x is nil")
    case int:
        println("the type of x is int")
    case string:
        println("the type of x is string")
    case bool:
        println("the type of x is string")
    default:
        println("don't support the type")
    }
}

我們看到,這個例子中 switch 語句的形式與前面是一致的,不同的是 switch 與 case 兩個關鍵字後面跟著的表示式。

switch 關鍵字後面跟著的表示式為x.(type),這種表示式形式是 switch 語句專有的,而且也只能在 switch 語句中使用。這個表示式中的 x 必須是一個介面型別變數,表示式的求值結果是這個介面型別變數對應的動態型別。

什麼是一個介面型別的動態型別呢?我們簡單解釋一下。以上面的程式碼 var x interface{} = 13 為例,x 是一個介面型別變數,它的靜態型別為interface{},如果我們將整型值 13 賦值給 x,x 這個介面變數的動態型別就為 int。關於介面型別變數的動態型別,我們後面還會詳細講,這裡先簡單瞭解一下就可以了。

接著,case 關鍵字後面接的就不是普通意義上的表示式了,而是一個個具體的型別。這樣,Go 就能使用變數 x 的動態型別與各個 case 中的型別進行匹配,之後的邏輯就都是一樣的了。

現在我們執行上面示例程式,輸出了 x 的動態變數型別:

the type of x is int

不過,透過 x.(type) ,我們除了可以獲得變數 x 的動態型別資訊之外,也能獲得其動態型別對應的值資訊,現在我們把上面的例子改造一下:

func main() {
    var x interface{} = 13
    switch v := x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v)
    case string:
        println("the type of v is string, v =", v)
    case bool:
        println("the type of v is bool, v =", v)
    default:
        println("don't support the type")
    }
}

這裡我們將 switch 後面的表示式由 x.(type) 換成了 v := x.(type) 。對於後者,你千萬不要認為變數 v 儲存的是型別資訊,其實 v 儲存的是變數 x 的動態型別對應的值資訊,這樣我們在接下來的 case 執行路徑中就可以使用變數 v 中的值資訊了。

然後,我們執行上面示例,可以得到 v 的動態型別和值:

the type of v is int, v = 13

另外,你可以發現,在前面的 type switch 演示示例中,我們一直使用 interface{}這種介面型別的變數,Go 中所有型別都實現了 interface{}型別,所以 case 後面可以是任意型別資訊。

但如果在 switch 後面使用了某個特定的介面型別 I,那麼 case 後面就只能使用實現了介面型別 I 的型別了,否則 Go 編譯器會報錯。你可以看看這個例子:

  type I interface {
      M()
  }
  
  type T struct {
  }
  
 func (T) M() {
 }
 
 func main() {
     var t T
     var i I = t
     switch i.(type) {
     case T:
         println("it is type T")
     case int:
         println("it is type int")
     case string:
         println("it is type string")
     }
 }

在這個例子中,我們在 type switch 中使用了自定義的介面型別 I。那麼,理論上所有 case 後面的型別都只能是實現了介面 I 的型別。但在這段程式碼中,只有型別 T 實現了介面型別 I,Go 原生型別 int 與 string 都沒有實現介面 I,於是在編譯上述程式碼時,編譯器會報出如下錯誤資訊:

19:2: impossible type switch case: i (type I) cannot have dynamic type int (missing M method)
21:2: impossible type switch case: i (type I) cannot have dynamic type string (missing M method)

五、跳不出迴圈的 break

這裡,我們來看一個找出整型切片中第一個偶數的例子,使用 switch 分支結構:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1

    // find first even number of the interger slice
    for i := 0; i < len(sl); i++ {
        switch sl[i] % 2 {
        case 0:
            firstEven = sl[i]
            break
        case 1:
            // do nothing
        }        
    }         
    println(firstEven) 
}

我們執行一下這個修改後的程式,得到結果為 12。

奇怪,這個輸出的值與我們的預期的好像不太一樣。這段程式碼中,切片中的第一個偶數是 6,而輸出的結果卻成了切片的最後一個偶數 12。為什麼會出現這種結果呢?

這就是 Go 中 break 語句與 switch 分支結合使用會出現一個“小坑”。和我們習慣的 C 家族語言中的 break 不同,Go 語言規範中明確規定,不帶 label 的 break 語句中斷執行並跳出的,是同一函式內 break 語句所在的最內層的 for、switch 或 select。所以,上面這個例子的 break 語句實際上只跳出了 switch 語句,並沒有跳出外層的 for 迴圈,這也就是程式未按我們預期執行的原因。

要修正這一問題,我們可以利用 labelbreak 語句試試。這裡我們也直接看看改進後的程式碼:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1

    // find first even number of the interger slice
loop:
    for i := 0; i < len(sl); i++ {
        switch sl[i] % 2 {
        case 0:
            firstEven = sl[i]
            break loop
        case 1:
            // do nothing
        }
    }
    println(firstEven) // 6
}

在改進後的例子中,我們定義了一個 label:loop,這個 label 附在 for 迴圈的外面,指代 for 迴圈的執行。當程式碼執行到“break loop”時,程式將停止 label loop 所指代的 for 迴圈的執行。

六、switch與if 比較

Go程式語言中的switch語句和if語句是用於控制程式流程的兩個不同工具,它們可以用來執行條件性程式碼塊,但它們在使用方式和適用場景上有所不同。

相似之處:

  • if語句和switch語句都用於根據某個條件執行不同的程式碼塊。
  • 兩者都可以用於處理多個條件或值的情況。

不同之處:

  • if語句通常用於處理更復雜的條件邏輯,可以檢查任何布林表示式。它是通用的條件控制工具。
  • switch語句專門用於根據一個表示式的值選擇執行不同的程式碼塊。它通常用於在多個值之間進行精確的比較。

if語句中,你可以編寫任意複雜的條件,例如:

if condition1 {
    // 當condition1為真時執行這裡的程式碼
} else if condition2 {
    // 當condition2為真時執行這裡的程式碼
} else {
    // 如果以上條件都不為真,執行這裡的程式碼
}

而在switch語句中,你主要是根據某個表示式的值進行選擇,比較簡潔:

switch expression {
case value1:
    // 當expression等於value1時執行這裡的程式碼
case value2:
    // 當expression等於value2時執行這裡的程式碼
default:
    // 如果expression不等於任何一個value,執行這裡的程式碼
}

使用if語句更適合處理複雜的條件邏輯,而switch語句更適合在多個值之間進行簡單的比較。

相關文章