Go 精簡(合并)了流控制語句,雖然某些時候不夠便捷,但夠用。
if...else...
條件表達式值必須是布爾類型,可省略括号,且左花括号不能另起一行。
func main() {
x := 3
if x > 5 {
println("a")
} else if x < 5 && x > 0 {
println("b")
} else {
println("z")
}
}
比較特别的是對初始化語句的支援,可定義塊局部變量或執行初始化函數。
func main() {
x := 10
if xinit(); x == 0 { // 優先執行 xinit 函數
println("a")
}
if a, b := x + 1, x + 10; a < b { // 定義一個或多個局部變量(也可以是函數傳回值)
println(a)
} else {
println(b)
}
}
局部變量的有效範圍包含整個 if/else 塊。
盡可能減少代碼塊嵌套,讓正常邏輯處于相同層次。
import (
"errors"
"log"
)
func check(x int) error {
if x <= 0 {
return errors.New("x <= 0")
}
return nil
}
func main() {
x := 10
if err := check(x); err == nil {
x++
println(x)
} else {
log.Fatal(err)
}
}
該示例中,if 塊雖然承擔了 2種 邏輯:錯誤處理 和 後續正常操作。基于重構原則,我們應該保持代碼塊功能的單一性。
func check(x int) error {
if x <= 0 {
return errors.New("x <= 0")
}
return nil
}
func main() {
x := 10
if err := check(x); err != nil {
log.Fatalln(err)
}
x++
println(x)
}
如此,if 塊僅完成條件檢查 和 錯誤處理,相關正常邏輯保持在同一層次。當有人視圖通過閱讀這段代碼來獲知邏輯流程時,完全可忽略 if 塊細節。同時,單一功能可提升代碼可維護性,更利于拆分重構。
當然,如須在多個條件塊中使用局部變量,那麼隻能保留層次,或直接使用外部變量。
func main() {
s := "9"
n, err := strconv.ParseInt(s, 10, 64) // 使用外部變量
if err != nil {
log.Fatalln(err)
} else if n < 0 || n > 10 { // 也可以考慮拆分成另一個獨立 if 塊
log.Fatalln("invalid number")
}
println(n) // 避免 if 局部變量将該邏輯放到 else 塊
}
對應某些過于複雜的組合條件,建議将其重構為函數。
func main() {
s := "9"
if n, err := strconv.ParseInt(s, 10, 64); err != nil || n < 0 || n > 10 || n % 2 != 0 {
log.Fatalln("invalid number")
}
println("ok")
}
函數調用雖然有一些性能損失,可卻讓主流程式變得更加清爽。況且,條件語句獨立之後,更易于測試,同樣會改善代碼可維護性。
func check(s string) error {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || n < 0 || n > 10 || n%2 != 0 {
return errors.New("invalid number")
}
return nil
}
func main() {
s := "9"
if err := check(s); err != nil {
log.Fatalln(err)
}
println("ok")
}
将 流程 和 布局 細節分離是很常見的做法,不同的變化因素被分隔在各自獨立單元(函數或子產品)内,可避免修改時造成關聯錯誤,減少患“肥胖症”的函數數量。當然,代碼單元測試也是主要原因之一。另一方面,該示例中的函數 check 僅被 if 塊調用,也可将其作為局部函數,以避免擴大作用域,隻是對測試的友好度會差一些。
目前編譯器隻能說夠用,須優化的地方太多,其中内聯處理做得也差強人意,是以代碼維護性 和 性能平衡需要投入更多心力。
語言方面,最遺憾的是沒有條件運算符 “a > b ? a : b”。有沒有 lambda 無所謂,但沒有這個卻少份優雅。加上一大堆 err != nil 判斷語句,對于有完美主義傾向的代碼潔癖患者來說是種折磨。
switch
與 if 類似,switch 語句也用于選擇執行,但具體使用場景會有所不同。
1、表達式 switch 語句
func main() {
a, b, c, x := 1, 2, 3, 2
switch x { // 将 x 與 case 條件比對
case a, b: // 多個比對條件命中其一即可(OR),變量
println("a | b")
case c: // 單個比對條件
println("c")
case 4: // 常量
println("d")
default:
println("z")
}
}
輸出:
a | b
條件表達式支援非常量值,這要比 C 更加靈活。相比 if 表達式,switch 值清單要更加簡潔。編譯器對 if、switch 生成的機器指令可能完全相同,所謂誰性能更好須看具體情況,不能作為主觀判斷條件。
switch 同樣支援初始化語句,按從上到下、從左到右順序比對 case 執行。隻有全部比對失敗時,才會執行 default 塊。
func main() {
switch x := 5; x {
default: // 編譯器確定不會先執行 default 塊
x += 100
println(x)
case 5:
x += 50
println(x)
}
}
55
考慮到 default 作用類似 else,建議将其放置在 switch 末尾。
相鄰的空 case 不構成多條件比對。
switch x { // 單條件,内容為空。隐式 "case a: break;"
case a:
case b:
println("b")
}
不能出現重複的 case 常量值。
func main() {
switch x := 5; x {
case 5:
println("a")
case 6, 5: // 錯誤:duplicate case 5 in switch
println("b")
}
}
無須顯式執行 break 語句,case 執行完畢後自動中斷。如須貫通後續 case (源碼順序),須執行 fallthrough,但不再比對後續條件表達式。
func main() {
switch x := 5; x {
default:
println(x)
case 5:
x += 10
println(x)
fallthrough // 繼續執行下一個 case,但不再比對條件表達式
case 6:
x += 20
println(x)
//fallthrough // 如果在此繼續 fallthrough,不會執行 default,完全按照源碼順序
// 導緻 "cannot fallthrough final case in switch" 錯誤
}
}
15
35
注意:fallthrough 必須放在 case 塊結尾,可使用 break 語句阻止。
func main() {
switch x := 5; x {
case 5:
x += 10
println(x)
if x >= 15 {
break // 終止,不再執行後續語句
}
fallthrough // 必須是 case 塊的最後一條語句
case 6:
x += 20
println(x)
}
}
15
某些時候,switch 還被用來替換 if 語句。被省略的 switch 條件表達式預設值為 true,繼而與 case 比較表達式結果比對。
func main() {
switch x := 5; { // 相當于 "switch x :=5; true { ... }"
case x > 5:
println("a")
case x > 0 && x <= 5: // 不能寫成 "case x > 0, x <= 5",因為多條件是 OR 關系
println("b")
default:
println("c")
}
}
b
2、類型 switch 語句
類型 switch 語句 将對類型進行判定,而不是值。下面是一個簡單的例子:
var v interface{}
// 省略了部分代碼
// v = 8
// v = "wenjianbao"
switch v.(type) {
case string:
fmt.Printf("The string is '%s'\n", v.(string))
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
fmt.Printf("The interger is %d\n", v)
default:
fmt.Printf("Unsupporte value.(type=%T)\n", v)
}
類型 switch 語句的 switch 表達式會包含一個特殊的類型斷言,例如 v.(type)。它雖然特殊,但是也要遵循類型斷言的規則。其次,每個 case 表達式中包含的都是 類型字面量,而不是表達式。最後,fallthrough 語句不允許出現在類型 switch 語句中。
類型 switch 語句的 switch 表達式還有一種變形寫法,如下:
var v interface{}
// 省略了部分代碼
// v = 8
// v = "wenjianbao"
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 interger is %d\n", i)
default:
fmt.Printf("Unsupporte value.(type=%T)\n", i)
}
這裡的 i := v.(type) 使經類型轉換後的值得以儲存。i 的類型一定會是 v 的值的實際類型。
for
僅有 for 一種循環語句,但常用方式都能支援。
for i := 0; i < 3; i++ { // 初始化表達式支援函數調用或定義局部變量
}
for x < 10 { // 類似 "while x < 10 {}" 或 "for ; x < 10; {}"
x++
}
for { // 類似 "while true {}" 或 "for true {}"
break
}
初始化語句僅被執行一次。條件表達式中如有函數調用,須确認是否會重複執行。可能會被編譯器優化掉,也可能是動态結果,須每次執行确認。
func count() int {
print("count.")
return 3
}
func main() {
for i, c := 0, count(); i < c; i++ { // 初始化語句的 count() 函數僅執行一次
println("a", i)
}
c := 0
for c < count() { // 條件表達式中的 count 重複執行
println("b", c)
c++
}
}
count.a 0
a 1
a 2
count.b 0
count.b 1
count.b 2
count.
Process finished with exit code 0
規避方式 就是在初始化表達式中定義局部變量儲存 count 結果。
可用 for ... range 完成資料疊代,支援 字元串、數組、數組指針、切片、字典、通道類型,傳回 索引、鍵值 資料。
data teyp | 1st value | 2nd value | |
---|---|---|---|
string | index | s[index] | unicode, rune |
array/slice | v[index] | ||
map | key | value | |
channel | element |
# 疊代字元串:
func main() {
str := "hello world"
for index, ch := range str {
fmt.Printf("%d -- %c\n", index, ch)
}
}
0 -- h
1 -- e
2 -- l
3 -- l
4 -- o
5 --
6 -- w
7 -- o
8 -- r
9 -- l
10 -- d
# 疊代數組
func main() {
data := [3]string{"a", "b", "c"}
for i, s := range data {
println(i, s)
}
}
0 a
1 b
2 c
沒有相關接口實作自定義類型疊代,除非基礎類型是上述類型之一。
允許傳回單值,或用 “_” 忽略。
func main() {
data := [3]string{"a", "b", "c"}
for i := range data { // 隻傳回 1st value
println(i, data[i])
}
for _, s := range data { // 忽略 1st value
println(s)
}
for range data { // 僅疊代,不傳回。可用來執行清空 channel 等操作
}
}
無論普通 for 循環,還是 range 疊代,其定義的局部變量都會 重複使用。
func main() {
data := [3]string{"a", "b", "c"}
for i, s := range data {
println(&i, &s)
}
}
0xc42003bef0 0xc42003bf08
0xc42003bef0 0xc42003bf08
0xc42003bef0 0xc42003bf08
這對 閉包 存在一些影響,相關詳情,請閱讀後續章節。
注意,range 會 複制 目标資料。受直接影響的是 數組,可改用 數組指針 或 切片 類型。
func main() {
data := [3]int{10, 20, 30}
for i, x := range data { // 從 data 複制品中取值
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}
fmt.Printf("x: %d, data: %d\n", x, data[i])
}
for i, x := range data[:] { // 僅複制 slice,不包括 底層 array
if i == 0 {
data[0] += 100
data[1] += 200
data[2] += 300
}
fmt.Printf("x: %d, data: %d\n", x , data[i])
}
}
x: 10, data: 110
x: 20, data: 220 // range 傳回的依舊是複制值
x: 30, data: 330
x: 110, data: 210 // 當 i == 0 修改 data 時,x 已經取值,是以是 110
x: 420, data: 420 // 複制的僅是 slice 自身,底層 array 依舊是原對象
x: 630, data: 630
相關資料類型中,字元串、切片基本結構是個很小的結構體,而 字典、通道 本身是指針封裝,複制成本都很小,無須專門優化。
如果 range 目标表達式是函數調用,也僅被執行一次。
func data() []int {
println("origin data.")
return []int{10, 20, 30}
}
func main() {
for i, x := range data() {
println(i, x)
}
}
origin data.
0 10
1 20
2 30
建議嵌套循環不要超過 2 層,否則會難以維護。必要時可剝離,重構為函數。
使用 range 子句,有 3 點需要注意,如下:
- 若對數組、切片 或 字元串值進行疊代,且 := 左邊隻有一個疊代變量時,一定要小心。這時隻會得到其中元素的索引,而不是元素本身;這很可能并不是你想要的。
- 疊代沒有任何元素的數組值、為 nil 的切片值、為 nil 的字典值 或 為 "" 的字元串值,并不會執行 for 語句中的代碼。for 語句在一開始就會直接結束執行。因為這些值的長度都為 0。
- 疊代為 nil 的通道值 會讓目前流程永遠阻塞在 for 語句上!
goto,continue,break
對于 goto 的讨伐由來已久,仿佛它是“笨蛋”标簽一般。可事實上,能在很多場合見到
它的身影,就連 Go 源碼裡都有很多。
$ cd go/src
$ grep -r -n "goto" *
單就 Go 1.6 的源碼統計結果,goto 語句就超過 1000 條有餘。很驚訝,不是嗎?雖然某些設計模式可用來消除 goto 語句,但在性能優先的場合,它能發揮積極作用。
使用 goto 前,須先定義标簽。标簽區分大小寫,且未使用的标簽會引發編譯錯誤。
func main() {
start: // 錯誤:label start defined and note used
for i := 0; i < 3; i++ {
println(i)
if i > 1 {
goto exit
}
}
exit:
println("exit.")
}
不能跳轉到其他函數,或内層代碼塊内。
func test() {
test:
println("test")
println("test exit.")
}
func main() {
for i := 0; i < 3; i++ {
loop:
println(i)
}
goto test // 錯誤:label test not defined
goto loop // 錯誤:goto loop jumps into block
}
和 goto 定義跳轉不同,break、continue 用于中斷代碼執行。
- break:用于 switch、for、select 語句,終止整個語句塊執行。
- continue:僅用于 for 循環,終止後續邏輯,立即進入下一輪循環。
func main() {
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 立即進入下一輪循環
}
if i > 5 {
break // 立即終止整個 for 循環
}
println(i)
}
}
1
3
5
func main() {
outer:
for x := 0; x < 5; x++ {
for y := 0; y < 10; y++ {
if y > 2 {
println()
continue outer
}
if x > 2 {
break outer
}
print(x, ":", y, " ")
}
}
}
0:0 0:1 0:2
1:0 1:1 1:2
2:0 2:1 2:2