Go流程控制與快樂路徑原則
一、流程控制基本介紹
流程控制是每種程式語言控制邏輯走向和執行次序的重要部分,流程控制可以說是一門語言的“經脈”。
那麼 Go 語言對分支與迴圈兩種控制結構的支援是怎麼樣的呢?針對程式的分支結構,Go 提供了 if
和 switch-case
兩種語句形式;我們就先從 Go 語言分支結構之一的 if 語句開始講起。
二、if 語句
2.1 if 語句介紹
if 語句是 Go 語言中提供的一種分支控制結構,它也是 Go 中最常用、最簡單的分支控制結構。它會根據布林表示式的值,在兩個分支中選擇一個執行。
2.2 單分支結構的 if 語句形式
單分支結構的if
語句包含一個條件表示式和一個要執行的程式碼塊。如果條件表示式的值為true
,則執行程式碼塊。如果條件表示式的值為false
,則程式碼塊將被跳過。以下是單分支結構的if
語句的一般形式:
if boolean_expression {
// 新分支
}
// 原分支
這個 if 語句中的程式碼執行流程就等價於下面這幅流程圖:
boolean_expression
是一個布林表示式,通常返回true
或false
。- 如果
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 :當a 和b 都為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