4. 流程控制
流程控制是编程语言的骨架。Go 在保留经典控制结构的同时,做了大量简化与增强:没有 while/do-while,没有三元运算符,switch 默认不穿透,defer 成为资源管理的核心工具。本章逐一拆解每个控制原语的正确用法与工程陷阱。
4.1 if-else 分支:简洁与作用域
Go 的 if 条件表达式不需要括号,但必须跟大括号。支持在条件前执行一个简单语句,常用于错误处理与局部变量声明。
核心语法:
if 初始化语句; 条件表达式 { ... } else if ... { ... } else { ... }
// 标准形式
if err := os.Remove("tmp.txt"); err != nil {
log.Printf("remove failed: %v", err)
return
}
// 注意:err 的作用域仅限于 if-else 块内
fmt.Println("remove success")
| 特性 | Go 做法 | 常见误区 |
|---|---|---|
| 条件括号 | 禁止使用 () 包裹条件 |
从 C/Java 带来的习惯 |
| 初始化语句 | 支持 if x := f(); x > 0 {} |
忘记变量作用域仅限 if 块 |
| else 换行 | } 与 else 必须在同一行 |
换行导致编译错误 |
| 三元运算符 | 不支持 ? : |
用完整 if-else 替代 |
工程建议:优先使用
if 初始化语句; 条件 {} 模式处理错误,将错误变量作用域限制在最小范围内,避免污染外层命名空间。
4.2 for 循环:三种形式覆盖所有场景
Go 只有 for 关键字,但通过三种变体覆盖了 while、do-while 和无限循环的全部能力。
// 形式一:完整三段式(类似 C 的 for)
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 形式二:条件式(类似 while)
n := 0
for n < 5 {
n++
}
// 形式三:无限循环
for {
// 直到 break 或 return
}
避坑:
for range 遍历时,迭代变量(value)是复用地址的。在 goroutine 闭包中直接取 value 的指针会导致所有 goroutine 读到最后一个元素的值。必须显式拷贝:v := value。
// 错误示例
for _, v := range slice {
go func() {
fmt.Println(v) // 所有 goroutine 可能打印同一个值
}()
}
// 正确做法
for _, v := range slice {
v := v // 创建局部副本
go func() {
fmt.Println(v)
}()
}
4.3 switch 语句:默认不穿透与类型判断
Go 的 switch 做了两项关键改进:每个 case 默认带 break,支持表达式列表。同时提供类型 switch 用于接口类型判断。
// 值 switch:默认不穿透
switch status {
case 200:
fmt.Println("OK")
case 301, 302:
fmt.Println("Redirect")
case 404:
fmt.Println("Not Found")
default:
fmt.Println("Unknown")
}
// 表达式 switch:case 中可以写任意布尔表达式
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
default:
grade = "C"
}
// 类型 switch:用于接口类型断言
var v interface{} = "hello"
switch t := v.(type) {
case string:
fmt.Println("string:", t)
case int:
fmt.Println("int:", t)
default:
fmt.Printf("unknown type: %T\n", t)
}
| 特性 | Go | C/Java |
|---|---|---|
| 穿透行为 | 默认不穿透,需显式 fallthrough |
默认穿透,需显式 break |
| case 表达式 | 支持逗号分隔多个值 | 仅支持单个值 |
| 类型 switch | 原生支持 .(type) |
需 instanceof + 强制转换 |
| 条件 switch | switch 后可不跟表达式 | 不支持 |
工程建议:类型 switch 是处理多态接口的首选方式,比
if-else type assertion 更清晰、更安全。注意 fallthrough 极少使用,滥用会破坏可读性。
4.4 goto 与标签:受限制的跳转
Go 保留了 goto,但做了严格限制:不能跳入内层代码块,不能跳过变量声明。主要用于错误集中处理和深层循环的跳出。
// 错误集中处理
err := step1()
if err != nil {
goto cleanup
}
err = step2()
if err != nil {
goto cleanup
}
// 正常流程
return nil
cleanup:
// 释放资源
rollback()
return err
// 跳出多层循环
outer:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i*j > 50 {
break outer // 直接跳出外层循环
}
}
}
避坑:
goto 不能跳入 for、switch、select 等代码块内部,也不能跳过变量声明语句。滥用 goto 会使控制流难以追踪,仅在错误处理和嵌套循环跳出场景中使用。
4.5 defer 延迟执行:资源管理的基石
defer 将函数调用推迟到当前函数返回前执行,采用后进先出(LIFO)顺序。是 Go 中管理文件、锁、连接等资源的惯用模式。
// 基本用法:成对的资源获取与释放
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // 函数返回时自动关闭
mu.Lock()
defer mu.Unlock() // 解锁
// 多个 defer:LIFO 顺序
defer fmt.Println("first") // 第三个执行
defer fmt.Println("second") // 第二个执行
defer fmt.Println("third") // 第一个执行
关键行为:defer 语句中的参数在声明时求值,而非执行时。如果需要在 defer 中访问函数最终返回值,使用命名返回值。
// 参数即时求值
x := 10
defer fmt.Println(x) // 输出 10,即使后面 x 被修改
x = 20
// 命名返回值:defer 可以修改返回值
func example() (result int) {
defer func() {
result += 10 // 修改最终返回值
}()
return 5
}
// 调用 example() 返回 15
| 场景 | 推荐模式 | 反模式 |
|---|---|---|
| 文件操作 | defer f.Close() 紧跟在 os.Open 之后 |
在函数末尾手动关闭,遗漏错误路径 |
| 互斥锁 | defer mu.Unlock() 紧跟在 mu.Lock() 之后 |
在多个 return 路径分别解锁 |
| 数据库连接 | defer rows.Close() 紧跟在 db.Query 之后 |
忘记关闭导致连接泄漏 |
| 性能敏感 | 避免在热点循环内使用 defer | 在每次迭代中 defer 资源释放 |
工程建议:defer 应紧跟在资源获取之后,形成视觉上的成对关系。不要在 defer 中执行可能阻塞或耗时的操作,因为 defer 在函数返回前执行,会延迟函数返回。
4.6 综合对比与选型指南
| 场景 | 推荐结构 | 理由 |
|---|---|---|
| 错误处理 | if err != nil { return } |
Go 惯用模式,清晰直接 |
| 多条件判断 | switch { case ... } |
比 if-else 链更易读 |
| 接口类型判断 | switch t := v.(type) |
类型安全,无需强制转换 |
| 集合遍历 | for range |
支持 slice/map/channel/string |
| 资源释放 | defer |
确保释放,避免遗漏 |
| 多层循环跳出 | break label 或 goto |
避免使用标志变量 |
核心原则:Go 的控制流设计追求线性、可预测、无惊喜。defer 是资源管理的基石,switch 是条件分支的首选,for range 是遍历的标准答案。理解这些原语的设计哲学,才能写出地道的 Go 代码。