if(分支结构)
1984 年图灵奖获得者、著名计算机科学家尼古拉斯·沃斯(Niklaus Wirth)提出过著名的**“程序 = 数据结构 + 算法”**的公式。
- 数据结构:基本数据类型和复合数据类型;
- 算法:对真实世界运作规律的抽象,是解决真实世界中问题的步骤。在计算机世界中,再复杂的算法都可以通过顺序、分支和循环这三种基本的控制结构构造出来。
针对程序的分支结构,Go 提供了 ==if 和 switch-case== 两种语句形式;
针对循环结构,Go 只保留了 ==for== 这一种循环语句形式。
# 1. if(分支结构)
# 1.1 if介绍
Go 语言是站在 C 语言等的肩膀之上诞生与成长起来的。Go 语言继承了 C 语言的很多语法,这里就包括控制结构。但 Go 也不是全盘照搬,而是在继承的基础上又加上了自己的一些优化与改进,比如:
- Go 坚持“一件事情仅有一种做法的理念”,只保留了 for 这一种循环结构,去掉了 C 语言中的 while 和 do-while 循环结构;
- Go 填平了 C 语言中 switch 分支结构中每个 case 语句都要以 break 收尾的“坑”;
- Go 支持了 type switch 特性,让“类型”信息也可以作为分支选择的条件;
- Go 的 switch 控制结构的 case 语句还支持表达式列表,让相同处理逻辑的多个分支可以合并为一个分支,等等。
if 语句是 Go 语言中提供的一种分支控制结构,它也是 Go 中最常用、最简单的分支控制结构。它会根据布尔表达式的值,在两个分支中选择一个执行。
if boolean_expression {
// 新分支
}
// 原分支
2
3
4
5
**分支结构是传统结构化程序设计中的基础构件,**这个 if 语句中的代码执行流程就等价于下面这幅流程图:
Go 的 if 语句的特点:
- 和 Go 函数一样,if 语句的分支代码块的左大括号与 if 关键字在同一行上,这也是 Go 代码风格的统一要求,gofmt 工具会帮助我们实现这一点;
- if 语句的布尔表达式整体不需要用括号包裹,一定程度上减少了开发人员敲击键盘的次数。而且,if 关键字后面的条件判断表达式的求值结果必须是布尔类型,即要么是 true,要么是 false:
if runtime.GOOS == "linux" {
println("we are on linux os")
}
2
3
# 1.2 操作符优先级
如果判断的条件比较多,可以用多个逻辑操作符连接起多个条件判断表达式
if (runtime.GOOS == "linux") && (runtime.GOARCH == "amd64") &&
(runtime.Compiler != "gccgo") {
println("we are using standard go compiler on linux os for amd64")
}
2
3
4
逻辑运算符:
Go 语言的操作符是有优先级的。这里要记住,一元操作符,比如上面的逻辑非操作符,具有最高优先级,其他操作符的优先级如下:
操作符优先级决定了操作数优先参与哪个操作符的求值运算:
func main() {
a, b := false,true
if a && b != true {
println("(a && b) != true")
return
}
println("a && (b != true) == false")
}
2
3
4
5
6
7
8
这段代码的关键就在于,if 后面的布尔表达式中的操作数 b 是先参与 && 的求值运算,还是先参与!= 的求值运算。
根据前面的操作符优先级表,我们知道,!= 的优先级要高于 &&,因此操作数 b 先参与的是!= 的求值运算,这样 if 后的布尔表达式就等价于 a && (b != true) ,而不是我们最初认为的 (a && b) != true。
# 1.3 if写法
# 1.3.1 最简形式
// 当 boolean_expression 求值为 true 时,执行 新分支,否则,执行 原分支:
if boolean_expression {
// 新分支
}
// 原分支
2
3
4
5
6
# 1.3.2 二分支结构
// 当 boolean_expression 求值为 true 时,执行分支 1,否则,执行分支 2:
if boolean_expression {
// 分支1
} else {
// 分支2
}
2
3
4
5
6
# 1.3.3 多(N)分支结构
if boolean_expression1 {
// 分支1
} else if boolean_expression2 {
// 分支2
} else if boolean_expression3 {
// 分支3
} else {
// 分支4
}
// 变换成二分之结构 如下:
if boolean_expression1 {
// 分支1
} else {
if boolean_expression2 {
// 分支2
} else {
if boolean_expression3 {
// 分支3
} else {
// 分支4
}
}
}
// 这样等价转换后,我们得到一个层层缩进的二分支结构,通过上面我们对二分支的分析,再来理解这个结构就十分容易了。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
其他例子:
package main
import "fmt"
func main() {
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C") // C
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.3 支持声明 if 语句的自用变量
无论是单分支、二分支还是多分支结构,都可以在 if 后的布尔表达式前,进行一些变量的声明,在 if 布尔表达式前声明的变量,我叫它 if 语句的自用变量。顾名思义,这些变量只可以在 if 语句的代码块范围内使用。
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)
}
}
2
3
4
5
6
7
8
9
好处:
- 直观上可以让开发者有一种代码行数减少的感觉,提高可读性;
- 由于这些变量是 if 语句自用变量,它的作用域仅限于 if 语句的各层隐式代码块中,if 语句外部无法访问和更改这些变量,这就让这些变量具有一定隔离性,这样你在阅读和理解 if 语句的代码时也可以更聚焦。
缺点:
- 注意“变量遮蔽”问题
在 if 表达式之前添加一个执行语句,再根据变量值进行判断。
package main
import "fmt"
func main() {
// 这里的 score 是局部作用域
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C") // C
fmt.Println(score) // 65 只能在函数内部打印 score
}
//fmt.Println(score) //undefined: score
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.4 if 语句的“快乐路径”原则
从可读性上来看,单分支结构要优于二分支结构,二分支结构又优于多分支结构。在日常编码中要减少多分支结构,甚至是二分支结构的使用,这会有助于我们编写出优雅、简洁、易读易维护且不易错的代码。
下面是两段逻辑相同但形式不同的伪代码段:
//伪代码段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
}
// 伪代码段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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
代码段 1 特点:
- 没有使用 else 分支,失败就立即返回;
- “成功”逻辑始终“居左”并延续到函数结尾,没有被嵌入到 if 的布尔表达式为 true 的代码分支中;
- 整个代码段布局扁平,没有深度的缩进;
伪代码段 2,使用了带有嵌套的二分支结构,它的特点如下:
- 整个代码段呈现为“锯齿状”,有深度缩进;
- “成功”逻辑被嵌入到 if 的布尔表达式为 true 的代码分支中;
很明显,伪代码段 1 的逻辑更容易理解,也更简洁。Go 社区把这种 if 语句的使用方式称为 if 语句的“快乐路径(Happy Path)”原则,所谓“快乐路径”也就是成功逻辑的代码执行路径,它的特点是这样的:
- 仅使用单分支控制结构;
- 当布尔表达式求值为 false 时,也就是出现错误时,在单分支中快速返回;
- 正常逻辑在代码布局上始终“靠左”,这样读者可以从上到下一眼看到该函数正常逻辑的全貌;
- 函数执行到最后一行代表一种成功状态。
Go 社区推荐 Gopher 们在使用 if 语句时尽量符合这些原则,如果你的函数实现代码不符合“快乐路径”原则,你可以按下面步骤进行重构:
- 尝试将“正常逻辑”提取出来,放到“快乐路径”中;
- 如果无法做到上一点,很可能是函数内的逻辑过于复杂,可以将深度缩进到 else 分支中的代码析出到一个函数中,再对原函数实施“快乐路径”原则。