Go流程控制與快樂路徑原則

2023-10-12 06:00:47

Go流程控制與快樂路徑原則

一、流程控制基本介紹

流程控制是每種程式語言控制邏輯走向和執行次序的重要部分,流程控制可以說是一門語言的「經脈」。

那麼 Go 語言對分支與迴圈兩種控制結構的支援是怎麼樣的呢?針對程式的分支結構,Go 提供了 ifswitch-case 兩種語句形式;我們就先從 Go 語言分支結構之一的 if 語句開始講起。

二、if 語句

2.1 if 語句介紹

if 語句是 Go 語言中提供的一種分支控制結構,它也是 Go 中最常用、最簡單的分支控制結構。它會根據布林表示式的值,在兩個分支中選擇一個執行。

2.2 單分支結構的 if 語句形式

單分支結構的if語句包含一個條件表示式和一個要執行的程式碼塊。如果條件表示式的值為true,則執行程式碼塊。如果條件表示式的值為false,則程式碼塊將被跳過。以下是單分支結構的if語句的一般形式:

if boolean_expression {
    // 新分支
}

// 原分支

這個 if 語句中的程式碼執行流程就等價於下面這幅流程圖:

  • boolean_expression是一個布林表示式,通常返回truefalse
  • 如果boolean_expression的值為true,則執行// 當條件為真時執行的程式碼部分的程式碼塊。
  • 如果boolean_expression的值為false,則程式碼塊將被跳過,繼續執行下一個語句。

2.3 Go 的 if 語句的特點

2.3.1 分支程式碼塊左大括號與if同行

if 語句的分支程式碼塊的左大括號與 if 關鍵字在同一行上,這是 Go 程式碼風格的統一要求,gofmt 工具會幫助我們實現這一點;

2.3.2 條件表示式不需要括號

if 語句的布林表示式整體不需要用括號包裹,這使得程式碼更加簡潔。而且,if 關鍵字後面的條件判斷表示式的求值結果必須是布林型別,即要麼是 true,要麼是 false

if runtime.GOOS == "darwin" {
    println("we are on MacOS")
}

如果判斷的條件比較多,我們可以用多個邏輯操作符連線起多個條件判斷表示式,比如這段程式碼就是用了多個邏輯操作符 && 來連線多個布林表示式:

	if (runtime.GOOS == "darwin") && (runtime.GOARCH == "amd64") &&
		(runtime.Compiler != "gccgo") {
		println("we are using standard go compiler on Mac os for amd64")
	}

上面範例程式碼中的每個布林表示式都被小括號括上了,這是為了降低你在閱讀和理解這段程式碼時,面對操作符優先順序的心智負擔。

三、操作符

3.1 邏輯操作符

邏輯操作符除了上面的 && 之外,Go 還提供了另外兩個邏輯操作符,如下表:

邏輯操作符 含義 表示式求值舉例
&& 邏輯與 a &&b:當ab都為true時,該表示式的求值 結果為true
` `
` ` 邏輯非

3.2 操作符的優先順序

一元操作符,比如上面的邏輯非操作符,具有最高優先順序,其他操作符的優先順序如下:

優先順序(從高到低) 操作符列表
5 *, /, %, <<, >>, &, &^
4 +, -
3 !=, ==, <, <=, >, >=
2 &&
1 ||
  • 優先順序5的是乘、除、取模和位元運算符
  • 優先順序4的是加法和減法運運算元
  • 優先順序3的是關係和相等運運算元
  • 優先順序2的是邏輯與
  • 優先順序最低的是邏輯或

操作符優先順序決定了運算元優先參與哪個操作符的求值運算,我們以下面程式碼中 if 語句的布林表示式為例:

func main() {
    a, b := false,true
    if a && b != true {
        println("(a && b) != true")
        return
    }
    println("a && (b != true) == false")
}

這段程式碼會輸出得到的是 a && (b != true) == false。這是為什麼呢?

這段程式碼的關鍵就在於,if 後面的布林表示式中的運算元 b 是先參與 && 的求值運算,還是先參與!= 的求值運算。根據前面的操作符優先順序表,我們知道,!= 的優先順序要高於 &&,因此運算元 b 先參與的是!= 的求值運算,這樣 if 後的布林表示式就等價於 a && (b != true)

針對以上問題,推薦在 if 布林表示式中,使用帶有小括號的子布林表示式來清晰地表達判斷條件

這樣做不僅可以消除了自己記住操作符優先順序的學習負擔,當其他人閱讀你的程式碼時,也可以很清晰地看出布林表示式要表達的邏輯關係,這能讓我們程式碼的可讀性更好,更易於理解,不會因記錯操作符優先順序順序而產生錯誤的理解。

三、if 多(N)分支結構

3.1 if else(分支結構)

Go語言中if else(分支結構)條件判斷的格式如下:

if boolean_expression {
  // 分支1
} else {
  // 分支2
}

3.2 if(N)分支結構(if ... else if ... else)

if條件(N)分支結構格式如下:

if boolean_expression1 {
  // 分支1
} else if boolean_expression2 {
  // 分支2

... ...

} else if boolean_expressionN {
  // 分支N
} else {
  // 分支N+1
}

我們以下面這個四分支的程式碼為例,看看怎麼拆解這個多分支結構:

if boolean_expression1 {
    // 分支1
} else if boolean_expression2 {
    // 分支2
} else if boolean_expression3 {
    // 分支3
} else {
    // 分支4
} 

以下是一個範例,演示如何使用if-else結構來判斷一個分數的等級:

package main

import "fmt"

func main() {
    score := 85

    if score >= 90 {
        fmt.Println("A")
    } else if score >= 80 {
        fmt.Println("B")
    } else if score >= 70 {
        fmt.Println("C")
    } else {
        fmt.Println("D")
    }
}

四、if 語句的自用變數

無論是單分支、二分支還是多分支結構,我們都可以在 if 後的布林表示式前,進行一些變數的宣告,在 if 布林表示式前宣告的變數,叫 if 語句的自用變數。顧名思義,這些變數只可以在 if 語句的程式碼塊範圍內使用,比如下面程式碼中的變數 a、b 和 c:

func main() {
    if a, c := f(), h(); a > 0 {
        println(a)
    } else if b := f(); b > 0 {
        println(a, b)
    } else {
        println(a, b, c)
    }
}

我們可以看到自用變數宣告的位置是在每個 if 語句的後面,布林表示式的前面,而且,由於宣告本身是一個語句,所以我們需要把它和後面的布林表示式通過分號分隔開。

在 if 語句中宣告自用變數是 Go 語言的一個慣用法,這種使用方式直觀上可以讓開發者有一種程式碼行數減少的感覺,提高可讀性。同時,由於這些變數是 if 語句自用變數,它的作用域僅限於 if 語句的各層隱式程式碼塊中,if 語句外部無法存取和更改這些變數,這就讓這些變數具有一定隔離性,這樣你在閱讀和理解 if 語句的程式碼時也可以更聚焦。

五、if 語句的「快樂路徑」原則

上面我們已經學了 if 分支控制結構的三種形式了,從可讀性上來看,單分支結構要優於二分支結構,二分支結構又優於多分支結構。那麼顯然,我們在日常編碼中要減少多分支結構,甚至是二分支結構的使用,這會有助於我們編寫出優雅、簡潔、易讀易維護且不易錯的程式碼。

首先,我們來看一段虛擬碼段1:

//虛擬碼段1:

func doSomething() error {
  if errorCondition1 {
    // some error logic
    ... ...
    return err1
  }
  
  // some success logic
  ... ...

  if errorCondition2 {
    // some error logic
    ... ...
    return err2
  }

  // some success logic
  ... ...
  return nil
}

我們看到單分支控制結構的虛擬碼段 1 有這幾個特點:

  • 沒有使用 else 分支,失敗就立即返回;
  • 「成功」邏輯始終「居左」並延續到函數結尾,沒有被嵌入到 if 的布林表示式為 true 的程式碼分支中;
  • 整個程式碼段佈局扁平,沒有深度的縮排;
  • 程式碼的可讀性很高

我們來看一段虛擬碼段2:

// 虛擬碼段2:

func doSomething() error {
  if successCondition1 {
    // some success logic
    ... ...

    if successCondition2 {
      // some success logic
      ... ...

      return nil
    } else {
      // some error logic
      ... ...
      return err2
    }
  } else {
    // some error logic
    ... ...
    return err1
  }
}

虛擬碼段 2 實現了同樣邏輯碼段 1,就使用了帶有巢狀的二分支結構,它的特點如下:

  • 整個程式碼段呈現為「鋸齒狀」,有深度縮排;
  • 「成功」邏輯被嵌入到 if 的布林表示式為 true 的程式碼分支中;

很明顯,虛擬碼段 1 的邏輯更容易理解,也更簡潔。Go 社群把這種 if 語句的使用方式稱為 if 語句的「快樂路徑(Happy Path)」原則,所謂「快樂路徑」也就是成功邏輯的程式碼執行路徑,它的特點是這樣的:

  • 僅使用單分支控制結構;

  • 當布林表示式求值為 false 時,也就是出現錯誤時,在單分支中快速返回;

  • 正常邏輯在程式碼佈局上始終「靠左」,這樣讀者可以從上到下一眼看到該函數正常邏輯的全貌;

  • 函數執行到最後一行代表一種成功狀態。

Go 社群推薦 Gopher 們在使用 if 語句時儘量符合這些原則,如果你的函數實現程式碼不符合「快樂路徑」原則,你可以按下面步驟進行重構:

  • 嘗試將「正常邏輯」提取出來,放到「快樂路徑」中;

  • 如果無法做到上一點,很可能是函數內的邏輯過於複雜,可以將深度縮排到 else 分支中的程式碼析出到一個函數中,再對原函數實施「快樂路徑」原則。

我的部落格即將同步至騰訊雲開發者社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1u9q67mdnb338