Wiki LogoWiki - The Power of Many

Week 02: 控制流、函数与错误处理

掌握 Go 的 for/if/switch 控制流, 函数定义与多返回值, 以及 Go 独特的错误处理模式.

1. 控制流概述

Go 的控制流语句极为简洁:

  • 只有 for 一种循环 (没有 while, do-while)
  • ifswitch 都可以包含初始化语句
  • 没有三元运算符 ? :

2. for 循环

2.1 标准 for 循环

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

三部分组成:

  1. 初始化语句 i := 0: 循环开始前执行一次
  2. 条件表达式 i < 10: 每次迭代前检查
  3. 后置语句 i++: 每次迭代后执行

2.2 while 风格

省略初始化和后置语句:

n := 0
for n < 5 {
    fmt.Println(n)
    n++
}

2.3 无限循环

for {
    fmt.Println("无限循环")
    // 使用 break 退出
    if shouldStop() {
        break
    }
}

2.4 range 遍历

range 用于遍历数组、切片、映射、字符串和通道.

// 遍历切片
nums := []int{1, 2, 3, 4, 5}
for index, value := range nums {
    fmt.Printf("索引: %d, 值: %d\n", index, value)
}

// 只需要值
for _, value := range nums {
    fmt.Println(value)
}

// 只需要索引
for index := range nums {
    fmt.Println(index)
}

// 遍历映射
m := map[string]int{"a": 1, "b": 2}
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

// 遍历字符串 (按 rune)
for i, r := range "你好" {
    fmt.Printf("%d: %c\n", i, r)
}
// 输出:
// 0: 你
// 3: 好  (注意索引是字节偏移, 不是字符索引)

2.5 break 和 continue

  • break: 跳出当前循环
  • continue: 跳过本次迭代, 进入下一次
for i := 0; i < 10; i++ {
    if i == 5 {
        break  // 完全退出循环
    }
    if i%2 == 0 {
        continue  // 跳过偶数
    }
    fmt.Println(i)  // 只打印 1, 3
}

2.6 标签 (Label)

用于在嵌套循环中跳出外层:

outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break outer  // 跳出外层循环
            }
            fmt.Printf("i=%d, j=%d\n", i, j)
        }
    }

3. if 语句

3.1 基本形式

x := 10

if x > 0 {
    fmt.Println("正数")
} else if x < 0 {
    fmt.Println("负数")
} else {
    fmt.Println("零")
}

注意: { 必须与 if 在同一行 (Go 的分号插入规则).

3.2 带初始化语句

Go 允许在 if 条件前添加一个初始化语句:

if err := doSomething(); err != nil {
    fmt.Println("错误:", err)
    return
}
// err 在此不可见 (作用域仅限 if 块)

这种模式在错误处理中非常常见, 保持了代码的紧凑性.

3.3 条件表达式必须是布尔值

// 错误! Go 不支持隐式转换
if 1 {  // 编译错误
}

// 正确
if x != 0 {
}

4. switch 语句

4.1 表达式 switch

day := "Monday"

switch day {
case "Monday":
    fmt.Println("周一")
case "Tuesday", "Wednesday":  // 多值匹配
    fmt.Println("周二或周三")
default:
    fmt.Println("其他")
}

与 C/Java 的区别:

  • 不需要 break: 默认只执行匹配的 case, 不会 fallthrough.
  • case 可以是表达式: 不限于常量.

4.2 无表达式 switch

等价于 switch true:

score := 85

switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 60:
    fmt.Println("C")
default:
    fmt.Println("F")
}

4.3 fallthrough

强制执行下一个 case (即使不匹配):

n := 1
switch n {
case 1:
    fmt.Println("一")
    fallthrough
case 2:
    fmt.Println("二")
}
// 输出:
// 一
// 二

4.4 类型 switch

用于判断接口变量的实际类型:

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("整数: %d\n", v)
    case string:
        fmt.Printf("字符串: %s\n", v)
    case bool:
        fmt.Printf("布尔: %t\n", v)
    default:
        fmt.Printf("未知类型: %T\n", v)
    }
}

describe(42)       // 整数: 42
describe("hello")  // 字符串: hello
describe(true)     // 布尔: true

5. 函数

5.1 函数定义

func add(a int, b int) int {
    return a + b
}

// 参数类型相同时可简写
func add(a, b int) int {
    return a + b
}

5.2 多返回值

Go 函数可以返回多个值, 这在错误处理中非常关键:

func divide(a, b int) (int, int) {
    return a / b, a % b
}

quotient, remainder := divide(17, 5)
fmt.Printf("%d / %d = %d%d\n", 17, 5, quotient, remainder)
// 17 / 5 = 3 余 2

5.3 命名返回值

可以为返回值命名, 它们会被初始化为零值:

func divide(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return  // 裸返回, 返回命名变量的当前值
}

注意: 裸返回在短函数中可以增加可读性, 但在长函数中应避免使用.

5.4 可变参数

使用 ... 声明可变参数:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)        // 6
sum(1, 2, 3, 4, 5)  // 15

// 传入切片需要展开
nums := []int{1, 2, 3}
sum(nums...)        // 6

5.5 函数作为值

Go 中函数是一等公民, 可以赋值给变量:

add := func(a, b int) int {
    return a + b
}
result := add(1, 2)  // 3

// 函数作为参数
func apply(fn func(int, int) int, a, b int) int {
    return fn(a, b)
}
apply(add, 3, 4)  // 7

5.6 闭包 (Closure)

闭包是一个函数值, 它引用了其外部作用域的变量:

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
fmt.Println(c())  // 1
fmt.Println(c())  // 2
fmt.Println(c())  // 3

闭包的内存模型: 当内部函数引用外部变量时, 该变量会逃逸到堆上, 其生命周期延长至闭包不再被引用.

// 闭包实际上是一个结构体
// type closureFunc struct {
//     code *func()  // 函数代码
//     count *int    // 捕获的变量 (在堆上)
// }

6. defer 语句

6.1 基本用法

defer 语句将函数调用推迟到当前函数返回前执行:

func main() {
    fmt.Println("开始")
    defer fmt.Println("结束")  // 延迟执行
    fmt.Println("处理中")
}
// 输出:
// 开始
// 处理中
// 结束

6.2 常见用途

资源清理:

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // 确保文件关闭
    
    // 读取文件...
    return nil
}

解锁:

mu.Lock()
defer mu.Unlock()
// 临界区代码...

记录耗时:

func process() {
    start := time.Now()
    defer func() {
        fmt.Printf("耗时: %v\n", time.Since(start))
    }()
    
    // 处理逻辑...
}

6.3 执行顺序 (LIFO)

多个 defer 按后进先出 (Last In, First Out) 顺序执行:

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// 输出:
// 3
// 2
// 1

6.4 参数求值时机

defer 函数的参数在 defer 语句执行时求值, 而不是在函数返回时:

func main() {
    i := 0
    defer fmt.Println(i)  // 参数 i 在此时求值 (0)
    i++
}
// 输出: 0 (不是 1)

如果需要捕获最终值, 使用闭包:

func main() {
    i := 0
    defer func() {
        fmt.Println(i)  // 闭包捕获 i 的引用
    }()
    i++
}
// 输出: 1

6.5 defer 与 return 的交互

命名返回值可以被 defer 修改:

func test() (result int) {
    defer func() {
        result++  // 修改返回值
    }()
    return 10  // result = 10
}
// 返回 11

执行顺序:

  1. result = 10 (return 赋值)
  2. result++ (defer 执行)
  3. RET (真正返回)

7. 错误处理

7.1 error 接口

Go 使用 error 接口表示错误:

type error interface {
    Error() string
}

7.2 创建错误

import "errors"

// 方式 1: errors.New
err := errors.New("something went wrong")

// 方式 2: fmt.Errorf (格式化)
err := fmt.Errorf("user %s not found", username)

7.3 错误处理模式

标准模式: 函数返回值的最后一个是 error, 调用后立即检查.

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 0)
if err != nil {
    fmt.Println("错误:", err)
    return
}
fmt.Println("结果:", result)

7.4 哨兵错误 (Sentinel Errors)

预定义的错误值, 用于精确匹配:

var ErrNotFound = errors.New("not found")
var ErrPermissionDenied = errors.New("permission denied")

func findUser(id int) (*User, error) {
    // ...
    return nil, ErrNotFound
}

user, err := findUser(123)
if err == ErrNotFound {
    fmt.Println("用户不存在")
}

7.5 自定义错误类型

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "cannot be negative",
        }
    }
    return nil
}

7.6 错误包装 (Go 1.13+)

使用 %w 包装错误, 保留错误链:

func readConfig() error {
    f, err := os.Open("config.yaml")
    if err != nil {
        return fmt.Errorf("open config: %w", err)
    }
    defer f.Close()
    // ...
}

errors.Is: 检查错误链中是否包含特定错误:

if errors.Is(err, os.ErrNotExist) {
    fmt.Println("文件不存在")
}

errors.As: 从错误链中提取特定类型:

var valErr *ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("验证错误: %s\n", valErr.Field)
}

8. panic 和 recover

8.1 panic

panic 用于不可恢复的错误, 会导致程序崩溃:

func mustOpen(path string) *os.File {
    f, err := os.Open(path)
    if err != nil {
        panic(err)  // 程序终止
    }
    return f
}

何时使用 panic:

  • 启动时配置错误 (无法继续运行)
  • 编程错误 (如数组越界, 这会自动 panic)
  • 库函数的 "Must" 变体 (如 regexp.MustCompile)

8.2 recover

recover 用于捕获 panic, 必须在 defer 函数中调用:

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    panic("something bad happened")
}
// 输出: Recovered from panic: something bad happened

func main() {
    safeCall()
    fmt.Println("程序继续运行")
}

8.3 使用原则

  1. 库代码应避免 panic: 返回 error 让调用者决定如何处理.
  2. recover 仅用于边界: 如 HTTP handler 顶层, 防止单个请求崩溃导致整个服务宕机.
  3. 不要用 panic 做常规错误处理: panic/recover 的性能开销比 error 大得多.

9. 错误处理进阶模式

9.1 自定义错误类型层次

在生产代码中, 常需要对错误进行分类以便统一处理:

type ErrCode string

const (
    ErrCodeNotFound    ErrCode = "NOT_FOUND"
    ErrCodePermission  ErrCode = "PERMISSION_DENIED"
    ErrCodeValidation  ErrCode = "VALIDATION_FAILED"
)

type AppError struct {
    Code    ErrCode
    Message string
    Cause   error
}

func (e *AppError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// 构造函数
func NewNotFoundError(resource string, cause error) *AppError {
    return &AppError{
        Code:    ErrCodeNotFound,
        Message: fmt.Sprintf("%s not found", resource),
        Cause:   cause,
    }
}

9.2 在 HTTP Handler 中统一处理错误

func handleError(w http.ResponseWriter, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        switch appErr.Code {
        case ErrCodeNotFound:
            http.Error(w, appErr.Message, http.StatusNotFound)
        case ErrCodePermission:
            http.Error(w, appErr.Message, http.StatusForbidden)
        default:
            http.Error(w, appErr.Message, http.StatusBadRequest)
        }
        return
    }
    http.Error(w, "internal error", http.StatusInternalServerError)
}

9.3 多错误聚合 (Go 1.20+)

errors.Join 用于将多个错误合并为一个:

func validateUser(u User) error {
    var errs []error
    
    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if u.Age < 0 {
        errs = append(errs, errors.New("age must be positive"))
    }
    if len(u.Email) == 0 {
        errs = append(errs, errors.New("email is required"))
    }
    
    if len(errs) > 0 {
        return errors.Join(errs...)  // 合并所有错误
    }
    return nil
}

// errors.Is 可以检测 Join 中的任意错误
err := validateUser(User{})
if errors.Is(err, ErrEmptyName) {
    // 处理
}

9.4 错误处理原则

原则说明
只处理一次错误要么返回给调用者, 要么记录日志, 不要两者都做
添加上下文使用 fmt.Errorf("operation: %w", err) 包装错误
类型化错误生产代码使用自定义错误类型, 便于分类处理
保留错误链使用 %w 而非 %v 以保留 errors.Is/As 能力

10. 练习

10.1 FizzBuzz

编写程序, 打印 1 到 100. 对于 3 的倍数打印 "Fizz", 5 的倍数打印 "Buzz", 同时是 3 和 5 的倍数打印 "FizzBuzz".

func main() {
    for i := 1; i <= 100; i++ {
        switch {
        case i%15 == 0:
            fmt.Println("FizzBuzz")
        case i%3 == 0:
            fmt.Println("Fizz")
        case i%5 == 0:
            fmt.Println("Buzz")
        default:
            fmt.Println(i)
        }
    }
}

10.2 递归阶乘

使用递归实现阶乘函数.

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

10.3 安全除法

实现一个不会 panic 的除法函数.

func safeDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

10.4 带重试的函数

实现一个函数, 在失败时重试最多 N 次.

func retry(n int, fn func() error) error {
    var err error
    for i := 0; i < n; i++ {
        err = fn()
        if err == nil {
            return nil
        }
        fmt.Printf("尝试 %d 失败: %v\n", i+1, err)
    }
    return fmt.Errorf("after %d attempts: %w", n, err)
}

11. 思考题

  1. Go 为什么只有 for 一种循环?
  2. defer 的 LIFO 顺序有什么实际意义?
  3. 为什么 Go 选择返回 error 而不是使用异常?
  4. errors.Is== 比较有什么区别?
  5. 闭包的变量逃逸对性能有什么影响?

12. 本周小结

  • 控制流: for (唯一循环), if (可带初始化), switch (不需要 break).
  • 函数: 多返回值, 命名返回值, 可变参数, 函数作为值, 闭包.
  • defer: 延迟执行, LIFO 顺序, 用于资源清理.
  • 错误处理: error 接口, errors.New, fmt.Errorf, 错误包装 (%w), errors.Is/As.
  • panic/recover: 用于不可恢复错误, 仅在边界处使用.

控制流和函数是编程的基础, 而 Go 独特的错误处理模式是理解 Go 代码风格的关键. 下周我们将学习复合类型.

On this page