在学习一门编程语言时,流程控制都是绕不过去的核心内容。程序之所以“有逻辑”,本质上就是因为它能够根据条件做判断、根据需要重复执行任务、在异常场景下进行兜底处理。
Go 语言在流程控制上看起来不算复杂,但它有几个非常鲜明的特点:语法简洁、约束明确、鼓励可读性优先,同时尽量减少“花哨但难维护”的写法。对于已经有其他语言基础、刚接触 Go 的读者来说,这一章不仅要会写,还要理解 Go 为什么这么设计。
本文会依次讲解:
- 条件语句:
if/else、带初始化的if - 循环语句:
for的多种形态 switch语句:表达式switch、类型switchdefer、panic、recover机制
为什么要重视流程控制
流程控制决定了程序的执行路径。一个程序即使数据结构设计得再好,如果判断逻辑混乱、循环边界不清、错误处理方式随意,最终也会变得难以维护。
Go 在流程控制上的设计思路可以概括为下面几点:
| 设计特点 | 说明 |
|---|---|
| 语法统一 | Go 只有一个循环关键字 for,减少语法分裂 |
| 强调简洁 | 没有三元表达式,鼓励清晰的条件判断 |
| 可读性优先 | 条件表达式不允许省略必要部分,也不鼓励过度嵌套 |
| 错误处理显式化 | 不主张把普通错误处理建立在异常机制上 |
理解这些设计原则后,再去看具体语法,就更容易明白 Go 的取舍。
一、条件语句:if / else
条件语句用于根据不同条件执行不同逻辑。Go 的 if 写法和很多语言类似,但也有两个明显特点:
- 条件表达式外不需要加括号
- 条件必须是布尔值,不能把整数、字符串等当成真假值使用
1. 基本 if / else 用法
下面的例子演示了如何根据分数判断考试结果:
package main
import "fmt"
func main() {
score := 78
if score >= 90 {
fmt.Println("成绩等级:优秀")
} else if score >= 60 {
fmt.Println("成绩等级:及格")
} else {
fmt.Println("成绩等级:不及格")
}
}
这段代码的执行逻辑非常直接:
- 如果分数大于等于 90,输出“优秀”
- 否则继续判断是否大于等于 60
- 如果也不满足,就进入
else
这里有几个需要注意的点:
if后面不写圆括号,例如if (score >= 90)虽然在一些语言里很常见,但 Go 不需要- 左花括号
{必须和条件语句写在同一行 else必须紧跟在上一个代码块后面,不能被自动换行断开
2. 逻辑运算配合 if 使用
实际开发中,一个判断往往不止一个条件。这时通常会结合逻辑运算符一起使用:
&&:并且||:或者!:取反
package main
import "fmt"
func main() {
age := 20
hasIDCard := true
if age >= 18 && hasIDCard {
fmt.Println("允许进入考场")
} else {
fmt.Println("不允许进入考场")
}
}
这个例子表示:年龄必须满 18 岁,并且持有身份证,才允许进入考场。
3. 带初始化语句的 if
这是 Go 中非常实用的一个写法。你可以在 if 判断之前,先执行一个初始化语句,这个变量只在当前 if/else 语句块中生效。
这种写法的优点是:
- 缩小变量作用域
- 避免临时变量污染外层代码
- 让判断逻辑更集中
package main
import "fmt"
func main() {
if length := len("Golang"); length > 5 {
fmt.Println("字符串长度大于 5,长度为:", length)
} else {
fmt.Println("字符串长度不大于 5,长度为:", length)
}
}
这里的 length := len("Golang") 就是初始化语句,后面的 length > 5 是判断条件。
要注意:
length只能在这个if...else结构内部使用- 离开这个结构后,变量就不可见了
这种写法在处理文件读取、查询结果判断、错误检查时非常常见。
4. if 使用建议
对于初学者来说,if 虽然简单,但很容易写得越来越复杂。可以遵循下面这些建议:
| 建议 | 说明 |
|---|---|
| 保持条件简单 | 一个 if 里不要堆太多复杂逻辑 |
| 优先处理特殊情况 | 可以减少层层嵌套 |
| 善用初始化语句 | 让临时变量作用域更清晰 |
| 条件必须明确为布尔值 | 不要沿用其他语言的“隐式真假”习惯 |
二、循环语句:for 的多种形态
很多语言会把循环分成 for、while、do...while 等不同形式,而 Go 只保留了一个关键字:for。
这看起来像是“语法少了”,实际上是“统一了表达方式”。你只要掌握 for,就基本掌握了 Go 的循环体系。
1. 传统 for 循环
这是最接近 C、Java 风格的写法,通常用于明确知道循环次数的场景。
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
fmt.Println("第", i, "次循环")
}
}
它由三部分组成:
- 初始化语句:
i := 1 - 条件表达式:
i <= 5 - 后置操作:
i++
执行流程如下:
- 先执行初始化语句
- 判断条件是否成立
- 条件成立则执行循环体
- 执行后置操作
- 再次判断条件,直到条件不成立为止
2. while 风格的 for
Go 没有 while 关键字,但可以通过只保留条件表达式来实现同样效果。
package main
import "fmt"
func main() {
n := 1
for n <= 5 {
fmt.Println("当前数字:", n)
n++
}
}
这段代码等价于很多语言里的:
while (n <= 5) {
...
}
这种写法适合“循环次数不确定,但循环条件明确”的场景。
3. 无限循环
如果省略 for 的条件部分,就会形成无限循环。它常用于:
- 持续监听任务
- 服务端请求处理
- 消费消息队列
- 配合
break在满足条件时退出
package main
import "fmt"
func main() {
count := 1
for {
fmt.Println("当前计数:", count)
if count == 3 {
fmt.Println("达到退出条件,结束循环")
break
}
count++
}
}
这个例子里,for 本身不会结束,但当 count == 3 时,通过 break 主动退出。
4. break 和 continue
在循环中,break 和 continue 是最常见的两个控制语句:
break:立即结束整个循环continue:跳过本次循环剩余代码,进入下一轮
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
if i == 3 {
fmt.Println("跳过数字 3")
continue
}
if i == 5 {
fmt.Println("遇到数字 5,结束循环")
break
}
fmt.Println("当前数字:", i)
}
}
运行逻辑如下:
- 当
i == 3时,不执行下面的打印数字逻辑,直接进入下一轮 - 当
i == 5时,循环直接结束
5. range 循环:遍历数组、切片、字符串、map
虽然本节重点是流程控制,但提到 for 时,不能不说 range。它是 Go 中非常高频的遍历方式。
遍历切片
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("下标=%d, 值=%d\n", index, value)
}
}
遍历字符串
package main
import "fmt"
func main() {
for index, r := range "Go语言" {
fmt.Printf("位置=%d, 字符=%c\n", index, r)
}
}
这里遍历的是 Unicode 字符,而不是简单的字节,这一点在处理中文时尤其重要。
遍历 map
package main
import "fmt"
func main() {
userAges := map[string]int{
"Alice": 24,
"Bob": 27,
"Carol": 22,
}
for name, age := range userAges {
fmt.Printf("%s 的年龄是 %d\n", name, age)
}
}
需要提醒的是:遍历 map 时,元素输出顺序通常不是固定的。Go 有意不保证 map 的遍历顺序,这也是为了避免开发者错误依赖“偶然顺序”。
6. for 循环总结
| 写法 | 示例形式 | 适用场景 |
|---|---|---|
| 传统循环 | for i := 0; i < n; i++ |
已知循环次数 |
| while 风格 | for condition |
条件控制型循环 |
| 无限循环 | for {} |
持续执行直到主动退出 |
| range 遍历 | for k, v := range data |
遍历集合类型 |
三、switch 语句
当一个变量需要根据多个分支进行判断时,switch 往往比一连串 if...else if...else 更清晰。
Go 的 switch 比很多语言更灵活,也更安全。它的两个典型特性是:
- 默认情况下每个
case执行完会自动结束,不需要手动写break case后面可以写多个匹配值,也可以不带表达式
1. 表达式 switch
这是最常见的用法,根据某个表达式的值进入不同分支。
package main
import "fmt"
func main() {
day := 3
switch day {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
case 4, 5:
fmt.Println("星期四或星期五")
default:
fmt.Println("周末")
}
}
这里的 case 4, 5 表示多个值共用同一段逻辑。
和很多语言不同,Go 中这个 switch 在匹配到某个分支后,不会自动“贯穿”执行后面的分支,因此一般不需要写 break。
2. 不带表达式的 switch
Go 允许你省略 switch 后面的表达式,这样每个 case 都可以写成一个条件判断。它本质上等价于更整洁的 if...else if...else。
package main
import "fmt"
func main() {
score := 85
switch {
case score >= 90:
fmt.Println("等级:优秀")
case score >= 75:
fmt.Println("等级:良好")
case score >= 60:
fmt.Println("等级:及格")
default:
fmt.Println("等级:不及格")
}
}
这种写法在区间判断中非常自然。
3. 类型 switch
类型 switch 常用于接口值的具体类型判断。在你还没有深入学到接口时,可以先把它理解为:当一个变量可能保存不同类型的值时,可以通过类型 switch 来分别处理。
package main
import "fmt"
func describe(value interface{}) {
switch v := value.(type) {
case int:
fmt.Printf("这是一个 int,值为 %d\n", v)
case string:
fmt.Printf("这是一个 string,值为 %s\n", v)
case bool:
fmt.Printf("这是一个 bool,值为 %t\n", v)
default:
fmt.Printf("未知类型:%T\n", v)
}
}
func main() {
describe(100)
describe("hello go")
describe(true)
describe(3.14)
}
这里的关键点是:
value的类型是interface{},表示它可以接收任意类型的值value.(type)只能出现在类型switch中- 每个
case对应一种具体类型
类型 switch 在框架开发、通用工具函数、序列化处理等场景中很常见。
4. switch 和 if 的选择建议
| 场景 | 更推荐的写法 |
|---|---|
| 两三个简单条件判断 | if / else |
| 同一个变量对应多个固定值 | 表达式 switch |
| 区间判断较多 | 不带表达式的 switch |
| 需要根据动态类型处理 | 类型 switch |
四、defer、panic、recover 机制
这一部分是 Go 流程控制里很有代表性的内容。它们不只是“语法点”,更体现了 Go 对错误处理和程序控制的设计哲学。
在很多语言里,开发者习惯用 try/catch/finally 来处理异常和收尾逻辑。但 Go 没有这套机制,而是提供了:
defer:延迟执行,常用于资源释放和收尾动作panic:触发严重错误,导致程序进入异常展开流程recover:在合适位置捕获panic,避免程序直接崩溃
Go 的核心思想不是“让异常成为常规控制流”,而是:
- 普通错误,用显式返回值处理
- 真正不可恢复或不该继续运行的问题,再考虑
panic - 必要时在系统边界处用
recover做保护
这和很多依赖 try/catch 的语言有明显区别。
1. defer:延迟执行
defer 表示把某个语句延迟到当前函数执行结束前再执行。无论函数是正常返回,还是因为 panic 中断,只要已经注册了 defer,它通常都会被执行。
最常见用途包括:
- 关闭文件
- 释放锁
- 记录日志
- 打印函数退出信息
package main
import "fmt"
func main() {
fmt.Println("开始执行 main")
defer fmt.Println("defer:最后执行")
fmt.Println("处理中...")
fmt.Println("main 即将结束")
}
执行顺序是:
- 输出“开始执行 main”
- 注册
defer - 输出“处理中...”
- 输出“main 即将结束”
- 函数返回前执行
defer
多个 defer 的执行顺序
多个 defer 会按照“后进先出”的顺序执行,也就是栈结构。
package main
import "fmt"
func main() {
defer fmt.Println("第一个 defer")
defer fmt.Println("第二个 defer")
defer fmt.Println("第三个 defer")
fmt.Println("main 函数执行中")
}
输出顺序会是:
main 函数执行中第三个 defer第二个 defer第一个 defer
2. panic:触发严重错误
panic 表示程序遇到了无法继续正常执行的严重问题。触发 panic 后,当前函数会立即停止后续普通语句执行,然后开始向外层函数逐层返回,同时执行已经注册的 defer。
package main
import "fmt"
func divide(a, b int) int {
if b == 0 {
panic("除数不能为 0")
}
return a / b
}
func main() {
fmt.Println("准备计算")
result := divide(10, 0)
fmt.Println("结果:", result)
}
这段代码中,如果除数为 0,就主动触发 panic。此时程序不会继续正常执行 fmt.Println("结果:", result)。
需要特别强调:
panic不是拿来替代普通错误处理的- 比如“用户输入不合法”“文件不存在”“网络请求失败”这类情况,通常应该返回
error panic更适合表示编程错误、系统状态损坏、严重不变量被破坏等场景
3. recover:捕获 panic
recover 的作用是拦截 panic,让程序从异常状态中恢复。但它只能在 defer 修饰的函数中生效。
package main
import "fmt"
func safeRun() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获到 panic:", err)
}
}()
fmt.Println("safeRun 开始执行")
panic("发生了严重错误")
}
func main() {
safeRun()
fmt.Println("main 继续执行")
}
这个例子里:
safeRun中发生了panic- 由于提前注册了
defer,并在其中调用了recover() - 所以
panic被捕获,程序没有直接崩溃 main函数后续仍能继续运行
4. defer + panic + recover 联合示例
下面这个例子更完整地展示三者如何协同工作:
package main
import "fmt"
func testRecover() {
defer fmt.Println("defer 1:最外层收尾")
defer func() {
if err := recover(); err != nil {
fmt.Println("defer 2:成功恢复 panic,错误信息:", err)
}
}()
defer fmt.Println("defer 3:panic 发生后依然会执行")
fmt.Println("函数开始执行")
panic("模拟运行时故障")
}
func main() {
testRecover()
fmt.Println("程序恢复后继续向下执行")
}
这个示例体现了几个关键事实:
- 发生
panic后,已注册的defer仍然会执行 defer执行顺序仍然是后进先出recover成功后,程序可以回到调用者继续执行
5. 与 try/catch 的对比:为什么 Go 不鼓励把 panic 当常规异常机制
很多初学者在接触 panic/recover 时,会下意识把它类比成其他语言里的 throw/try-catch。这种类比有一定帮助,但不能完全等同。
下面是一个更准确的对比:
| 对比项 | Go 的 panic/recover |
常见语言的 try/catch |
|---|---|---|
| 主要用途 | 处理严重异常或程序不变量被破坏 | 常作为通用异常处理机制 |
| 日常错误处理 | 以 error 返回值为主 |
可能大量依赖异常抛出 |
| 恢复位置 | 通常在 defer 中恢复 |
在 catch 代码块中捕获 |
| 设计倾向 | 不鼓励把异常作为常规流程控制 | 某些语言中异常更常见 |
Go 的设计哲学非常明确:普通错误应该被显式处理,而不是隐藏在异常控制流里。
这样设计有几个好处:
- 代码阅读者一眼就能看出哪里可能出错
- 错误处理路径更加显式,不容易被忽略
- 不会因为滥用异常而让控制流变得隐蔽
所以在 Go 中,比较推荐的思路是:
- 能返回
error,就优先返回error - 只有在真正“程序已经不应该继续执行”的情况下才使用
panic - 在服务入口、协程边界、任务调度框架等位置,视情况用
recover做保护
五、一个综合示例:把流程控制串起来
学习完单独语法后,最好再看一个综合示例,把 if、for、switch、defer 串起来理解。
package main
import "fmt"
func processNumbers(nums []int) {
defer fmt.Println("处理结束,执行收尾工作")
for _, num := range nums {
if num < 0 {
fmt.Println("跳过负数:", num)
continue
}
switch {
case num == 0:
fmt.Println("数字是 0")
case num%2 == 0:
fmt.Println(num, "是偶数")
default:
fmt.Println(num, "是奇数")
}
}
}
func main() {
nums := []int{1, 2, 0, -3, 5, 8}
processNumbers(nums)
}
这个例子里:
for range负责遍历数据if负责过滤负数continue负责跳过当前项switch负责分类输出defer负责函数结束时的统一收尾
这就是流程控制在真实代码中的典型组合方式。
六、结语
Go 的流程控制语法不算多,但非常实用。它的设计重点不在“提供尽可能多的语法糖”,而在“让常见逻辑写得足够直接、稳定、易读”。
回顾一下本文的重点:
if / else用于条件分支,支持带初始化语句的写法for是 Go 唯一的循环关键字,但可以覆盖传统循环、while风格和无限循环switch不仅能判断值,还能做区间判断和类型判断defer适合做收尾,panic表示严重异常,recover用于受控恢复- Go 不鼓励把
panic/recover当成日常错误处理手段,这与很多依赖try/catch的语言有本质区别
如果你已经理解了这些内容,那么在阅读和编写 Go 代码时,就能更自然地掌握程序“该怎么走、什么时候停、出错后怎么收尾”。这也是写出清晰 Go 代码的重要一步。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!