编程语言代码的执行顺序,本质上说就是 “顺序” 和 “跳转” 这两种。顺序,就是按照代码书写顺序从上至下、从左至右(有优先级提权的先执行)的顺序依次执行(底层实际上是按照程序指令存储器上的存放顺序逐条执行)。跳转,就是在顺序执行中遇到跳转语句就跳转到指定的某处开始继续顺序执行。
这两种顺序组合在一起,就实现了我们的根据需求的业务逻辑而设计的代码控制逻辑。这种控制代码逻辑走向的过程就叫流程控制,而改变代码逻辑走向的语句就称之为流程控制语句。本节我们详细介绍 Go 语言的基本流程控制语句,包括 if、else、for、range、break、continue、switch、case、default、fallthrough、goto 等。
关键字 if 是用于判断其后面的某个条件(结果为布尔型)的语句。若该条件成立(结果为真),则执行 if 后面由花括号 {} 括起来的代码块,否则忽略该代码块,跳转到后面的代码继续执行。
Go 语言的 if 语句一些特点:
● if 后面的条件语句不需要用小括号括起来;
● 界定代码块的左花括号必须在 if 所在行的行尾;
● if 后面可以有一个简式声明变量并初始化的语句,该变量的作用域为这个 if 语句整个代码块(含与 else 组合的)。
下面分别介绍 if 不同组合的判断分支语句。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 8
if a > 5 { // 如果 a 大于 5 则执行 fmt.Println("a 大于 5"),否则跳到 15 行继续执行
fmt.Println("a 大于 5")
}
if a == 8 { // 如果 a 等于 8 则执行 fmt.Println("a 等于 8"),否则跳到 19 行继续执行
fmt.Println("a 等于 8")
}
if a < 5 { // 如果 a 小于 5 则执行 fmt.Println("a 小于 5"),否则跳到 23 行继续执行
fmt.Println("a 小于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 大于 5
a 等于 8
判断结束
如果将第10行的变量值改成 4 ,则修改后代码编译执行结果如下:
a 小于 5
判断结束
如果将第10行的变量值改成 5 ,则修改后代码编译执行结果如下:
判断结束
通过给变量 a 三次分别赋不同的值,得到的三个输出结果,可以看出每个 if 语句都是独立执行的。只有满足 if 后面的条件,该 if 后面花括号括起来的代码才会被执行。否则,该 if 后面花括号括起来的代码块被忽略了,直接跳到了该花括号的右花括号后面继续执行了。
在上面的判断中,无论前面的条件成立与否,后面的判断都要继续执行。通常我们知道 a 大于 5 了,我们就不需要再判断是不是小于 5 了,知道等于 8 了,也不需要判断是否大于 5 和小于 5 了,那这个逻辑结果我们怎样用代码逻辑实现呢?这就有了下面的 if else 的组合。
// 主函数,程序入口
func main() {
a := 5
if a == 8 {
fmt.Println("a 等于 8")
} else if a > 5 { // 如果 a 不等于 8,那继续判断是否 a 大于 5
fmt.Println("a 大于 5")
} else if a < 5 { // 如果 a 不等于 8 也不大于 5,那继续判断是否 a 小于 5
fmt.Println("a 小于 5")
} else { // 如果 a 不等于 8 也不大于 5 也不小于5,那就执行本代码块(实际就是等于 5)
fmt.Println("a 等于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 等于 5
判断结束
上面这个示例,我们可以尝试修改第10行变量 a 的值,无论你改成多少,数据结果总是只有一个判断结果出来,不会像上一个示例那样会有输出两个判断结果的时候。那说明这种写法,这些判断条件是 互斥 的,只要有一个成立,其他的条件就都不再判断了,其他 if 后面的花括号括起来的代码块也不会被执行了。
那么根据本示例,就可这样通俗理解:if 就是如果的意思,如果后面条件成立就执行本行花括号内的代码块;else if 就是否则继续判断后面的条件是否成立,也就是前面条件不成立则继续判断的意思;else 就是前面条件都不成立就执行这里的代码块,也就是其他情况到这里执行的意思。这里所有的条件只要有一个成立,就会跳出整组判断,直接跳到第 21 行继续执行,不会理会其他条件了。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 6
if b := 5; a == 8 {
fmt.Println("a 等于 8")
} else if a > b {
fmt.Println("a 大于 5")
} else if a < b {
fmt.Println("a 小于 5")
} else {
fmt.Println("a 等于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 大于 5
判断结束
本示例代码的逻辑与上一个示例完全相同,唯一区别就是在第12行增加了一个变量 b 的初始化声明,然后将原来在第14、16行的 a 与 5 的比较修改成 a 与 b 的比较,而 b 的值初始化为 5,所以结果完全相同。如果在有多个条件中使用了同一个值,那么这种方式可以避免一旦修改这个值要修改多行,方便代码维护。
但要注意,这个变量 b 的作用域仅限于这组 if 判断语句,也就是 12~20 行之间,离开这个区域就无效了。您可以尝试在第21行加一个打印输出语句,输出变量 b,会报未定义的错误。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 6
if b := 5; a == 8 {
fmt.Println("a 等于 8")
} else { // 如果 a == 8 不成立就从这里执行
if a > b {
fmt.Println("a 大于 5")
} else if a < b {
fmt.Println("a 小于 5")
} else {
fmt.Println("a 等于 5")
}
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 大于 5
判断结束
将上一个示例修改一下,使用 if 嵌套的形式,结果是一样的。所谓 if 嵌套,就是在 if 的代码块中还有 if 判断语句。
应尽量减少 if 语句嵌套的层级,如果有过多的条件需要判断,可以考虑封装函数的办法减少在同一个代码块中判断太多。这样可以减少代码的复杂度,以使代码更易于阅读和理解。
switch 语句会根据传入的参数值,从上至下检测复合条件的 case 分支。
Go 语言的 switch case 语句一些特点:
● switch 后面可以没有表达式。如果后面没有表达式,则 case 子句就是返回布尔值的表达式,也就是 case 匹配布尔值,而不是匹配与 switch 后面的表达式是否相等,这就如同 if else if else 语句的作用;
● switch 后面的表达式的值可以是任何可以比较的类型,不局限于整数;
● case 匹配成功后,执行完本段代码块就会直接跳出整个 switch 语句,不需要 break,不会继续执行后面的其他 case 子句。
● 如果需要继续执行后面的 case 子句,需要使用 fallthrough 关键字来强制执行。
● default 子句与 case 子句平级,但要所有 case 子句不匹配的情况下才会执行 default 子句,且 default 子句放在前面还是后面乃至其他中间位置都不影响这个规则。也是可以不写 default 子句的。
● switch 后面也是可以跟一个简式变量声明初始化语句的,同样其作用域仅限于 switch 语句代码块范围内。
下面分别介绍 switch case 语句的不同用法。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 6
switch b := 5; { // 这里加入了局部变量初始化语句,通常没有,根据需要决定
case a == 8: // 每个 case 都是一个结果为布尔值的表达式,
fmt.Println("a 等于 8")
case a > b:
fmt.Println("a 大于 5")
case a < b:
fmt.Println("a 小于 5")
default: // 所有 case 都匹配失败(条件都不成立)从这里执行
fmt.Println("a 等于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 大于 5
判断结束
这个示例逻辑上替代了上一个示例,执行结果完全一致。因 switch 后面没有需要匹配的表达式(变量初始化语句不算),所以下面的 case 就匹配布尔值,若真就算匹配成功,这样 case 后面就可以是比较判断表达式了。也是从上至下依次判断,但 default 子句除外,它放在哪里都是最后匹配的。只要有一个子句匹配成功,执行完该代码块都会跳出整个 switch 语句。
这里在 switch 关键字后面也使用了变量初始化语句,作用与 if 里的相同。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 6
switch a { // 这表示后面的 case 都要匹配 a 的值,也就是 case 后面的值 是否等于 a
case 8: // 表示如果 a 等于 8 则从这里执行
fmt.Println("a 等于 8")
case 5:
fmt.Println("a 等于 5")
case 4:
fmt.Println("a 等于 4")
default: // 表示上面 case 都匹配失败,到这里执行
fmt.Println("a 不等于 8、5、4 中的任何一个值")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 不等于 8、5、4 中的任何一个值
判断结束
本示例中 switch 后面跟了一个变量 a,这就是有表达式了,那后面的 case 就要配置 a 的值,与其相等表示匹配成功,执行该 case 下面的代码块。default 分支如果不需要可以不写,那样就是所有 case 匹配失败就跳出整个 switch 语句,继续执行后面的代码,也就是跳到本例中的 22 行继续执行。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 8
switch {
case a == 8:
fmt.Println("进入 a == 8 分支了")
break
fmt.Println("a 等于 8")
case a > 5:
fmt.Println("a 大于 5")
case a < 5:
fmt.Println("a 小于 5")
default:
fmt.Println("a 等于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
进入 a == 8 分支了
判断结束
通过示例的编译运行结果,可以看出 break 关键字在 switch 中的作用。第13行匹配成功后,开始进入 case 内执行第14行代码,输出了 “进入 a == 8 分支了” 字符串。然后顺序执行到第15行 break,则第16行的输出语句及下面所有的 case 没有再被执行,说明是跳出了整个 switch 语句。因为如果只是跳出当前的 case 子句,那么第17行的 case 应该也会匹配成功,然而实际没有看到其内部的输出语句的输出结果。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
a := 8
switch {
case a == 8:
fmt.Println("a 等于 8")
fallthrough
case a > 5:
fmt.Println("a 大于 5")
fallthrough
case a < 5:
fmt.Println("a 小于 5")
default:
fmt.Println("a 等于 5")
}
fmt.Println("判断结束")
}
上述代码编译执行结果如下:
a 等于 8
a 大于 5
a 小于 5
判断结束
从本例的输出结果可以看出,fallthrough 关键字是让其所在的 case 下面的 case 继续执行的作用。但仔细看第15行和第18行这两个 fallthrough 后面的 case 的条件是不同的,有一个是成立的,有一个是不成立的,所以 fallthrough 是直接跳到下一个的 case 的代码块内继续执行,而不是跳到下一个 case 那里继续匹配。
为什么没有输出 “a 等于 5” 这串字符,因为第21行上面没有 fallthrough 关键字,就不会穿透进来。
for 语句是 Go 语言里唯一的一个循环语句。
Go 语言的 for 循环语句一些特点:
● for 后面的表达式不需要用小括号括起来;
● 写无限循环不需要写分号,直接写一个 for 关键字就可以了;
● 界定代码块的左花括号必须在 for 所在行的行尾。
● 可以省略变量初始化语句,使用外部变量。
下面分别介绍 for 语句的不同用法。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
for i := 0; i < 5; i++ {
fmt.Println("循环计数变量的值:", i)
}
}
上述代码编译执行结果如下:
循环计数变量的值: 0
循环计数变量的值: 1
循环计数变量的值: 2
循环计数变量的值: 3
循环计数变量的值: 4
第10行的 i:=0; 表示初始化变量 i 从 0 开始计数;i<5; 表示如果 i 的值小于 5 就循环,否则结束循环,是先判断再循环;i++ 表示每循环一次 i 的值就加 1,是先循环后加 1。
当 i 被加到 4 的时候仍然小于 5,继续循环,执行花括号内的代码,然后 i 再加 1 就变成了 5,然后还是要先判断再循环,判断 5 不小于 5,所以条件不成立,循环结束。变量 i 就是用于循环计数的,这与其他大部分语言基本一样。
注意,这个变量 i 是 for 语句代码块的局部变量,其作用域仅限于本代码块内,在其外面再使用它是找不到的,如在第12行后面插入一行输出变量 i 的语句,是会报错的。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
i := 0
for ; i < 5; i++ {
fmt.Println("循环内变量的值:", i)
}
fmt.Println("最后循环外变量的值:", i)
}
上述代码编译执行结果如下:
循环内变量的值: 0
循环内变量的值: 1
循环内变量的值: 2
循环内变量的值: 3
循环内变量的值: 4
最后循环外变量的值: 5
这个示例就是把上个示例中的变量初始化语句只留下一个分号,然后直接使用外部的变量做循环计数。这样变量是在外部声明初始化的,所以循环结束后,外面还是可以使用的。
从输出的结果看,最后变量的值是 5,也验证了的确是 i 加到 5 后才结束的循环。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
i := 0
for ; i < 5; i++ {
if i == 3 {
continue
}
fmt.Println("循环内变量的值:", i)
}
fmt.Println("最后循环外变量的值:", i)
}
上述代码编译执行结果如下:
循环内变量的值: 0
循环内变量的值: 1
循环内变量的值: 2
循环内变量的值: 4
最后循环外变量的值: 5
本示例在上一个示例基础上增加了一个 if 判断,在循环计数变量 i 等于 3 的时候执行 continue 命令(第13~15行代码),结果我们发现编译运行后输出的结果里,没有了变量值 3。这充分说明了 continue 关键字的作用,是结束本轮循环,跳入下一轮循环继续执行循环。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
i := 0
for {
if i > 3 {
break // 跳出当前循环
}
fmt.Println("循环内变量的值:", i)
i++
}
fmt.Println("循环结束")
}
上述代码编译执行结果如下:
循环内变量的值: 0
循环内变量的值: 1
循环内变量的值: 2
循环内变量的值: 3
循环结束
for 关键字后面什么也不写,直接使用花括号定义代码块,这就是无限循环,什么时候停止,决定代码块内什么时候执行 break return 这类的跳转语句。所以写无限循环,一定不要忘记写跳出的条件,否则就成了一个死循环了。
本示例中就是通过判断变量被加到大于 3 的时候,执行 break,跳出循环。那么 break 在循环内的作用就很明显了,就是跳出当前循环。第17行的 i++ 的作用不可忽视,否则 i 一直停留在 0 上,就会死循环了。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
i := 0
for ; ; i++ { // 省略了循环条件
if i == 3 { // 什么时候停止循环,由这里空值
break
}
fmt.Println("循环内变量的值:", i)
}
fmt.Println("最后循环外变量的值:", i)
}
上述代码编译执行结果如下:
循环内变量的值: 0
循环内变量的值: 1
循环内变量的值: 2
最后循环外变量的值: 3
本示例是在 for 语句中省略了变量初始化及循环条件,由循环体(for 的花括号括着的代码段)中的判断条件控制循环何时结束。还可以有下面的形式:
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
i := 0
for i < 3 { // 只保留了循环条件
fmt.Println("循环内变量的值:", i)
i++ // 循环计数变量自增放在了循环体里
}
fmt.Println("最后循环外变量的值:", i)
}
上述代码编译执行结果如下:
循环内变量的值: 0
循环内变量的值: 1
循环内变量的值: 2
最后循环外变量的值: 3
本示例中在 for 后面只保留了循环条件,将循环计数变量的自增放到了循环体内了。
for 循环中使用 range 关键字可对数组、切片、map 等集合数据进行元素的遍历,在前面的章节中有介绍过,这里再加深一下理解。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
m := map[string]int{"one": 1, "two": 2}
fmt.Println("遍历 map,获取键名和值:")
for k, v := range m { // 键名(索引)和元素值都接收
fmt.Println(k, "->", v)
}
fmt.Println()
fmt.Println("遍历 map,只获取键名:")
for k := range m { // 只接收键名(索引)
fmt.Println(k)
}
fmt.Println()
fmt.Println("遍历 map,只获取值:")
for _, v := range m { // 只接收元素值
fmt.Println(v)
}
fmt.Println("循环结束")
}
上述代码编译执行结果如下:
遍历 map,获取键名和值:
one -> 1
two -> 2
遍历 map,只获取键名:
one
two
遍历 map,只获取值:
1
2
循环结束
遍历数组、切片只要将 range 后面的变量换成该类型的变量即可,其他没有什么不同,只是 map 里的键名等同于数组、切片里的索引这样理解就可以了。
使用 for 循环 配合 range 遍历集合类数据时,不需要设置循环计数变量,会自动从集合的顶部依次向下获取元素。range 遍历每次都会返回元素的 索引(键名) 和 元素值,索引在前元素值在后,所以需要两个变量来接,同样这两个变量也是 for 语句代码块的局部变量。
如果只需要索引(键名),那就直接用一个变量来接,range 会自动抛弃 元素值,见示例的第19行;如果只需要元素值,那还是要两个变量来接,只是将第一个变量改成下划线 “_”,写成匿名变量,这样接到后就会抛弃,但占位了,不影响元素值的接收。
Go 语言支持使用 标签(Lable) 来表示代码块,以用于 goto、break、continue 语句的定位跳转,标签是用字符串命名的自定义标识符,标注一个代码位置。
▲ goto 是强制跳转语句,使用时其后面跟有标签名,表示强制跳转到该位置去继续执行。goto 语句的特点:
● 只能在函数内跳转,不能跳出函数外
● 只能跳转到同级作用域或上级作用域,不能跳转到当前代码块内部的作用域。
● goto 后面同级代码不能有局部变量声明。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
func abc() {
goto aaa
fmt.Println("标签之前的输出")
aaa:
fmt.Println("标签之后的输出")
}
// 主函数,程序入口
func main() {
abc()
}
上述代码编译执行结果如下:
标签之后的输出
在第11行定义了一个标签 aaa,在执行 abc 函数时,代码是从上至下执行的,执行到第9行时,指令要求跳转到标签 aaa 处去执行,所以第10行没有被执行。
▲ break 在 for、switch 中的作用前面已经描述过了,但是如果其后面跟一个标签名,那就不仅仅是结束当前 for 循环或 switch 匹配判断,并直接跳转到相应名字的标签处继续执行。但是要求标签和 break 必须在同一个函数内。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
func abc() {
aaa: // 定义了一个标签 aaa
for i := 0; i < 3; i++ {
for j := 0; j < 3; i++ {
if i > 1 {
break aaa // 结束内部这个循环,然后直接跳到上面的标签 aaa 处继续执行
}
fmt.Println(i, j)
}
fmt.Println("第一个循环内的输出")
}
fmt.Println("第一个循环外的输出")
}
// 主函数,程序入口
func main() {
abc()
}
上述代码编译执行结果如下:
0 0
1 0
第一个循环外的输出
本例中如果不使用标签,应该会执行一次第17行代码,输出一行“第一个循环内的输出”。使用了标签之后,在第13行 break 结束循环的同时,将程序执行指针直接跳到了第9行的位置,所以与内部循环平级的第17行输出语句就没有被顺序执行。
▲ continue 在 for 循环中的作用前面已经描述过了,但是如果其后面跟一个标签名,那就不仅仅是结束 for 循环当前的这一轮,并直接跳转到相应名字的标签处继续执行。同样要求标签和 break 必须在同一个函数内。
将上面示例中的 break 替换成 continue,会发现结果是一模一样的。
select 是专门为监听通道通信设计的,每个 case 必须是一个通信操作,要么是发送要么是接收。语法与switch类似,这个在后面涉及到并发编程的章节中单独介绍,这里就不过多描述了。
函数调用及函数内的 return 语句也会引发流程的跳转,这些内容会在介绍函数的章节中介绍,这里也不过多描述了。
.
.
上一节:Go语言实践[回顾]教程21–详解Go语言的空值、零值、nil
下一节:Go语言实践[回顾]教程23–详解Go语言函数的声明、变参、参数传递
.