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 在函数返回前执行,会延迟函数返回。
Go 流程控制全景图 if-else 分支 • 条件无需括号 • 支持初始化语句 • 变量作用域限块内 • 无三元运算符 • else 必须同行 for 循环 • 三段式:for init; cond; post • 条件式:for cond {} • 无限循环:for {} • for range 遍历 • 迭代变量地址复用 switch 语句 • 默认不穿透 • 支持表达式列表 • 类型 switch • 条件 switch • fallthrough 显式穿透 goto 与标签 • 不能跳入内层块 • 不能跳过声明 • 错误集中处理 • 跳出多层循环 • 标签 + break/continue defer 延迟执行 • LIFO 执行顺序 • 参数即时求值 • 可修改命名返回值 • 资源管理惯用模式 • 紧跟在资源获取后 工程要点 • 控制流越简单越好 • defer 成对出现 • 避免深层嵌套 • 优先使用 for range • switch 比 if-else 链更清晰

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 代码。