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
语句会造成代码难以追踪和维护,非必需不使用。