Go 语言流程控制
基本概念
流程控制指使用选择、循环和跳转等结构来控制程序执行流程,以响应不同的输入和条件。对应到 Go 语言中,流程控制语句分为四类:条件语句、选择语句、循环语句和跳转语句。
条件语句
条件语句根据条件表达式结果来执行不同代码块。
单条件分支
如果条件语句仅有一个条件分支,则称为单分支选择结构。单分支选择结构使用单个 if
语句实现:
if condition {
// 条件为真时执行的代码
}
if
语句检查条件表达式 condition
的值,为 true
时执行代码块中代码,否则直接跳过:
package main
import (
"fmt"
"runtime"
)
func main() {
// 使用 if 语句判断系统是否需要特殊处理
if runtime.GOOS == "linux" {
fmt.Println("Linux 平台区分大小写,统一转为小写。")
}
// 程序继续执行其他任务
fmt.Println("正式开始运行")
}
双条件分支
如果条件语句有两个条件分支,则称为双分支选择结构。双分支选择结构使用 if...else
语句来实现:
if condition {
// 条件为真时执行
} else {
// 条件为假时执行
}
在 if
语句基础上,当 condition
为 false
时,会执行 else
语句块中代码:
package main
import (
"fmt"
"runtime"
)
func main() {
// 使用 if...else 语句判断是否要启用多线程
if runtime.NumCPU() > 1 {
fmt.Println("启动多线程运行")
} else {
fmt.Println("使用单线程运行")
}
}
多条件分支
如果条件语句有多个分支条件,则称该为多分支选择结构。多分支选择结构使用 else if
语句来扩展条件语句:
if condition1 {
// 条件 1 为真时执行
} else if condition2 {
// 条件 1 为假且条件 2 为真时执行
} else {
// 上述条件均为假时执行
}
当有多个条件需要判断时,else if
语句能在前面判断条件为 false
时接着判断下一个条件:
package main
import (
"fmt"
"time"
)
func main() {
// 获取当前时间(24 小时制)
hour := time.Now().Hour()
// 使用多个 else if 判断当前时间段
if hour < 6 {
fmt.Println("Good night!")
} else if hour < 12 {
fmt.Println("Good morning!")
} else if hour < 18 {
fmt.Println("Good afternoon!")
} else {
fmt.Println("Good evening!")
}
}
初始化语句
条件语句支持在条件检查前执行一个初始化子语句(表达式),用于声明局部变量。局部变量只在 if
和 else
代码块中有效:
if initialization; condition {
// 条件为真时执行
}
初始化子语句用于处理错误情况,能使代码更精简:
package main
import (
"fmt"
"os"
)
func main() {
// 使用初始化子语句,获取文件打开结果
if file, err := os.Open("filename"); err == nil {
// 文件打开成功,在这里操作
defer file.Close()
} else {
fmt.Println(err)
}
}
选择语句
选择语句和条件语句同属于条件选择语句,条件语句多用于单分支和双分支选择条件,选择语句表达多分支判断时更清晰。
Go 语言改进了传统 switch
语法设计,每个 case
是独立代码块,运行完自动结束语句,不需要显式使用 break
语句跳出。选择语句又分为三种主要格式或用法:有表达式切换、无表达式切换和类型切换。
有表达式
最常用格式,在 switch
后跟一个表达式,通常是一个变量。然后根据这个表达式的值来选择执行哪个 case
:
switch expression {
case value1:
// 代码块1
case value2, value3:
// 代码块2
default:
// 默认代码块
}
expression
:需要评估的表达式。case value1
、case value2, value3
…:与表达式值进行匹配的常量或变量,类型必须一致,不允许重复判断。每个case
可以拥有多个匹配值,只要匹配中任意一个都会执行代码块。default
:可选,没有任何case
匹配时执行的代码块,类似于else
语句。也可以不放在最后,但每个switch
语句只能存在一个默认分支。
如下面代码用来判断系统类型:
package main
import (
"fmt"
"runtime"
)
func main() {
os := runtime.GOOS
switch os {
case "linux":
fmt.Println("Linux 平台区分大小写,统一转为小写。")
case "windows":
fmt.Println("Windows 平台不区分大小写,无需转换。")
case "darwin", "freebsd":
fmt.Println("不支持此系统平台,程序即将退出。")
}
}
无表达式
switch
语句后可以不带表达式,而是在每个 case
中使用条件表达式判断,类似于 if...else if
语句:
switch {
case condition1:
// 当 condition1 为真时执行
case condition2:
// 当 condition2 为真时执行
default:
// 无条件匹配时执行
}
下面用选择语句替代多分支条件语句:
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 6:
fmt.Println("晚安")
case t.Hour() < 12:
fmt.Println("早安")
case t.Hour() < 18:
fmt.Println("下午好")
default:
fmt.Println("晚上好")
}
}
类型切换
类型切换是使用 switch
结构来检查接口变量的动态类型,并根据类型执行不同代码块:
package main
import "fmt"
func typeSwitch(i interface{}) {
switch v := i.(type) { // 如果没用到变量 v,可以直接写成 switch i.(type) {}
case int:
fmt.Printf("%v 乘以 2 等于 %v\n", v, v*2)
case string:
fmt.Printf("字符串 %q 长度为 %v 字节\n", v, len(v))
default:
fmt.Printf("不能处理类型:%T\n", v)
}
}
func main() {
typeSwitch(21) // 输出:21 乘以 2 等于 42
typeSwitch("hello") // 输出:字符串 "hello" 长度为 5 字节
typeSwitch(true) // 输出:不能处理类型:bool
}
类型判断中,不允许使用 fallthrough
关键字。
初始化语句
和条件语句一样,选择语句支持将一个初始化子语句写在关键字 switch
之后,初始化变量的作用域被限制在选择语句内部,避免外部使用这些变量:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 无表达式中初始化子语句,获取当前时间并赋值给 t
switch t := time.Now(); {
case t.Hour() > 9:
fmt.Println("努力工作")
default:
fmt.Println("晚安睡觉")
}
// 有表达式中初始化子语句,使用相同局部变量名 t
switch t := runtime.NumCPU(); t {
case 1:
fmt.Println("使用单线程运行")
default:
fmt.Println("启动多线程运行")
}
}
注意在无表达式 switch
中,初始化子语句后一定要有分号 ;
,不可以省略。
穿透
选择语句关键字 fallthrough
用于实现特殊控制流。规则如下:
- 定义位置:
fallthrough
必须是case
代码块中最后一个语句。不能用在最后一个case
或default
块中,因为没有下一个case
块可以执行。 - 无条件执行:在使用
fallthrough
关键字的case
块执行完毕后,下一个case
的条件不会被检查,块内代码会无条件执行。
使用 fallthrough
可以模拟传统编程语言中 switch
语句的穿透行为,在实际编程中一般用不到:
package main
import (
"fmt"
)
func main() {
i := 2
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
fallthrough
case 3: // 下降到这一分支
fmt.Println("three")
case 4: // 但不会影响到这一分支
fmt.Println("four")
default:
fmt.Println("something else")
}
}
循环语句
循环语句用来重复执行语句块,直到满足停止条件。Go 语言中没有 do...while
语句,因此 for
语句应用更广泛。
基本循环
for
基本循环结构包含初始化语句、条件语句和后续语句。语句之间使用分号来分隔,三个语句顺序不能错位:
for initialization; condition; post {
// 循环体
}
initialization
:初始化表达式仅在初次迭代时执行,给循环变量赋初值。condition
:条件表达式会在每次迭代前检查,为真时执行循环体中语句,否则跳出循环。post
:后续操作在每次迭代后执行,一般修改循环变量的值,然后跳到条件表达式继续评估。
基本循环与其他语言中的 for
循环用法类似,用于精确控制循环次数:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
// Go 1.22 之后,每次迭代的循环变量都是唯一的,因此会打印出不同指针值
// 在这之前 i 只会初始化一次,每次循环只是对同个 i 进行赋值
fmt.Println(&i, i)
}
}
可以有多个循环变量同时参与循环控制:
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Printf("请猜一个 0 到 100 之间的整数:\n")
for n, i := rand.Intn(100)+1, 0; i != n; fmt.Scan(&i) {
switch {
case i > n:
fmt.Println("猜大了,请继续:")
case i < n:
fmt.Println("猜小了,请继续:")
}
}
fmt.Printf("猜中了")
}
循环变量一般习惯使用 i
、j
、k
等较短的名称命名计数器,不要在循环体内修改计数器。
条件循环
for
语句可以省略初始化语句和后续语句,得到类似 while
语句的条件循环结构:
for condition {
// 循环体
}
这种形式只在循环顶部检查条件,用于循环次数不确定的场景。例如等待用户输入,直到输入特定内容,结束循环:
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("输入 exit 退出循环:")
for scanner.Scan() { // 循环读取用户输入
input := scanner.Text() // 获取用户输入的文本
// 当用户输入 exit 时,倒计时并退出循环
if input == "exit" {
// 嵌套循环退出倒计时
fmt.Println("退出倒计时:")
i := 5
for i > 0 {
fmt.Println(i)
time.Sleep(1 * time.Second)
i--
}
fmt.Println("退出循环")
break
}
fmt.Println("输入内容:", input)
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "读取错误:", err)
}
}
无限循环
如果进一步忽略 for
语句的条件表达式,会形成无限循环结构:
for {
// 循环体
}
无限循环可以用来持续监听或者将退出条件放在循环体中:
package main
import (
"fmt"
"time"
)
func main() {
for {
fmt.Println("每隔十秒,无限打印当前时间", time.Now())
time.Sleep(10 * time.Second)
}
}
遍历集合
for
循环可以配合 range
关键字,来遍历数组、切片、字符串、映射或通道类型中的元素:
package main
import "fmt"
func main() {
for i, s := range []string{"a", "b", "c", ""} {
fmt.Println("索引:", i) // 分别打印 0,1,2,3
fmt.Println("值:", s) // 分别打印 a b c
}
}
range
子语句作用类似于迭代器,针对不同遍历对象会返回不同值:
遍历对象 | 第一个返回值 | 第二个返回值 |
---|---|---|
字符串 | 索引 | 索引对应的值,类型为字符 |
数组和切片 | 索引 | 索引对应的值 |
映射 | 键 | 键对应的值 |
通道 | 元素,通道内的数据 |
对于空映射或切片、空数组、空字符串等情况,for
语句会直接结束。如果数组指定了长度,则 for
会循环执行长度相等的次数:
package main
import "fmt"
func main() {
// 不会执行
for _, x := range []int{} {
fmt.Printf("执行结果:%v\n", x)
}
// 循环 4 次,只有索引 0 为 1,其他索引返回 0
for i, x := range [4]int{1} {
fmt.Printf("索引 %d 的值:%v\n", i, x)
}
}
跳转语句
跳转语句用于跳出流程、跳过循环或跳转到指定标签位置。Go 语言中有三个跳转语句:goto
、break
和 continue
。
标签
标签用来在函数内标记位置,由自定义标识符后跟一个冒号 :
组成,需要和跳转语句配合使用:
- 与
goto
使用:goto
后面跟标签名,程序执行将跳转到标签位置。 - 与
break
和continue
使用:在循环嵌套或条件分支结构中,使用break
或continue
只作用于最内层循环,而配合标签可以跳转到具体的循环层次。
标签名建议使用大写字母和数字。
跳出
break
语句用于跳出包含它的最内层循环或选择结构。具体来说:
- 在
for
循环中:中断当前循环,不再执行后续迭代。 - 在
switch
语句中:从一个分支明确跳出整个switch
语句。 - 在
select
语句中:结束select
语句。
break
主要是用在循环语句中:
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
break // 到 i 等于 5 时跳出循环
}
fmt.Println(i) // 从 0 打印到 4
}
fmt.Println("继续执行代码")
}
和标签搭配使用,可以跳出指定循环:
package main
import "fmt"
func main() {
OuterLoop: // 标签用于外层循环
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 2 {
break OuterLoop // 跳出外层循环
}
fmt.Printf("i = %d, j = %d\n", i, j) // 只打印两行结果
}
}
fmt.Println("继续执行代码")
}
没有标签时,break
在内层 for
循环 2 次后中断,但外层还是会循环 3 次,总共打印 6 行结果。标签用在这里,直接跳出外层 for
循环,可减少很多判断代码。
跳过
continue
语句用于跳过当前循环迭代剩余语句,直接开始下一次迭代:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
if i%2 == 0 {
continue // i 等于偶数时跳过打印
}
fmt.Println(i) // 输出奇数 1 和 3
}
fmt.Println("继续执行代码")
}
配合标签,用于跳转到指定层级继续循环:
package main
import "fmt"
func main() {
OuterLoop: // 外层循环标签
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 2 {
continue OuterLoop // 条件满足直接跳到外层循环继续迭代
}
fmt.Printf("i = %d, j = %d\n", i, j) // 打印 6 行结果
}
}
fmt.Println("继续执行代码")
}
代码只改了跳出关键字,和 break
比起来,continue
不会中断循环语句,仅仅是跳过本次循环,继续从标签处开始迭代。
跳到
goto
语句用于无条件跳转到同一函数内的指定标签处。goto
语句可以用在任何地方,但不能跳过变量声明,也不能跳进一个代码块内部:
package main
import "fmt"
func test() {
L1:
goto L1
}
func main() {
fmt.Println("正常代码")
//goto L1 // 不能跨函数跳转
goto L2
fmt.Println("无效代码")
//a := 4 // 不能跳过变量声明
L2:
fmt.Println("跳到此处")
}
使用 go
语句会造成代码难以追踪和维护,非必需不使用。