4. Go 语言基础回顾(三):流程控制、函数与错误处理

在报表回放工程中,逻辑分支、循环遍历、函数封装与错误处理是代码骨架。本章聚焦 Go 语言流程控制、函数特性及错误处理机制,为后续工程落地打下坚实基础。

4.1 流程控制:if/else、for、switch

Go 语言的流程控制语法简洁,没有括号包裹条件表达式,但强制使用花括号。这在报表回放中用于判断数据状态、循环处理记录集、分支选择执行路径。

4.1.1 if/else 条件分支

Go 的 if 支持在条件前执行一个简单语句,常用于初始化局部变量并立即判断。报表回放中,常用于检查文件是否存在、数据是否有效。

// 标准 if/else
if score >= 90 {
    fmt.Println("优秀")
} else if score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

// 带初始化语句的 if
if err := loadReport("daily.rpt"); err != nil {
    fmt.Println("加载报表失败:", err)
    return
}

4.1.2 for 循环

Go 只有 for 关键字,但支持三种形式:经典三段式、条件循环、无限循环。报表回放中,for range 用于遍历切片、map 和通道。

循环形式 语法示例 工程场景
经典 for for i := 0; i < n; i++ { } 按索引遍历固定长度的记录列表
条件 for for retry > 0 { } 重试机制,直到成功或次数耗尽
无限 for for { select { ... } } 事件循环、监听通道消息
for range for k, v := range reportMap { } 遍历报表字段映射、数据行
// for range 遍历切片
records := []string{"record_001", "record_002", "record_003"}
for idx, record := range records {
    fmt.Printf("索引 %d: %s\n", idx, record)
}

// 遍历 map
reportMeta := map[string]string{"title": "日报", "author": "张三"}
for key, value := range reportMeta {
    fmt.Printf("%s = %s\n", key, value)
}

4.1.3 switch 分支选择

Go 的 switch 默认不穿透(无需 break),且支持表达式列表和类型断言。在报表回放中,常用于根据报表类型选择不同的解析器。

// 表达式 switch
switch reportType {
case "daily", "weekly":
    fmt.Println("常规报表")
case "monthly":
    fmt.Println("月度汇总")
default:
    fmt.Println("未知类型")
}

// 类型 switch (interface 类型断言)
var data interface{} = "string_value"
switch v := data.(type) {
case string:
    fmt.Println("字符串:", v)
case int:
    fmt.Println("整数:", v)
default:
    fmt.Println("未知类型")
}
工程提示:在报表回放中,switch 配合 fallthrough 关键字可实现穿透逻辑,但建议仅在需要连续执行多个 case 时使用,避免逻辑混乱。
Go 流程控制结构图 if / else if / else for 循环 switch 分支 条件判断 + 初始化 经典 / 条件 / 无限 / range 表达式 / 类型断言 工程要点: • if/else 支持初始化语句,变量作用域仅限于分支块内 • for range 遍历 map 时顺序随机,不可依赖顺序 • switch 默认不穿透,使用 fallthrough 需谨慎

4.2 函数:多返回值、defer、匿名函数

Go 函数是一等公民,支持多返回值、延迟执行(defer)和匿名函数(闭包)。在报表回放工程中,这些特性用于资源释放、错误传播和回调处理。

4.2.1 多返回值

Go 函数可以返回多个值,通常用于返回结果和错误。报表回放中,解析函数常返回数据对象和错误信息。

// 多返回值函数
func parseReport(path string) ([]Record, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("读取文件失败: %w", err)
    }
    records, err := unmarshalRecords(data)
    if err != nil {
        return nil, err
    }
    return records, nil
}

// 调用时使用多变量接收
records, err := parseReport("daily.rpt")
if err != nil {
    log.Fatal(err)
}

4.2.2 defer 延迟执行

defer 将函数调用推迟到当前函数返回前执行,常用于关闭文件、解锁互斥锁、释放数据库连接。多个 defer 按后进先出(LIFO)顺序执行。

func processReport(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close() // 函数结束时关闭文件

    // 加锁
    mu.Lock()
    defer mu.Unlock() // 函数结束时解锁

    // 业务逻辑
    return nil
}
避坑指南:defer 语句中的参数在 defer 声明时即求值,而非执行时。若需使用变量当前值,应传入指针或使用闭包捕获。

4.2.3 匿名函数与闭包

匿名函数没有函数名,可直接赋值给变量或作为参数传递。闭包捕获外部变量,在报表回放中用于构造回调、延迟计算和中间件。

// 匿名函数赋值
maxFunc := func(a, b int) int {
    if a > b {
        return a
    }
    return b
}
result := maxFunc(10, 20)

// 闭包:计数器
func newCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

counter := newCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
Go 函数核心特性关系图 函数 (func) 多返回值 defer 延迟执行 匿名函数 / 闭包 工程场景: 多返回值 → 返回结果+错误;defer → 资源释放/解锁;闭包 → 回调/状态保持

4.3 错误处理:error 接口与 panic/recover

Go 没有异常机制,错误通过返回值传递。标准库 error 是一个内置接口,任何实现了 Error() string 方法的类型都可作为错误。报表回放工程中,错误处理是保证稳定性的关键。

4.3.1 error 接口与自定义错误

// 标准错误创建
err := errors.New("文件未找到")

// 带格式化信息的错误
err := fmt.Errorf("解析报表 %s 失败: %w", reportName, innerErr)

// 自定义错误类型
type ReportError struct {
    Code    int
    Message string
}

func (e *ReportError) Error() string {
    return fmt.Sprintf("错误码 %d: %s", e.Code, e.Message)
}

// 使用 errors.As 进行类型断言
var reportErr *ReportError
if errors.As(err, &reportErr) {
    fmt.Println("错误码:", reportErr.Code)
}

4.3.2 panic 与 recover

panic 用于不可恢复的错误,recover 用于捕获 panic 并恢复执行。在报表回放中,仅在初始化阶段或严重异常时使用 panic,业务逻辑中优先返回 error。

func safeProcess(data []byte) (result string, err error) {
    defer func() {
        if r := recover(); r != nil {
            // 将 panic 转为 error 返回
            err = fmt.Errorf("处理过程发生 panic: %v", r)
        }
    }()

    // 可能触发 panic 的代码
    result = riskyOperation(data)
    return result, nil
}
最佳实践:在报表回放工程中,遵循“少用 panic,多用 error”原则。panic 仅用于程序初始化失败、配置严重错误等不可恢复场景;业务逻辑中所有可能失败的操作都应返回 error。

4.3.3 错误处理策略对比

策略 适用场景 工程示例
返回 error 业务逻辑中可预期的错误 文件不存在、数据格式错误
panic/recover 不可恢复的严重错误 配置缺失、内存不足、断言失败
errors.Is / As 错误链判断与类型断言 判断是否为特定错误类型
fmt.Errorf + %w 错误包装,保留原始错误链 多层调用时传递上下文
// 错误链示例
func loadConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("读取配置失败: %w", err)
    }
    // 解析配置...
    return nil
}

func main() {
    err := loadConfig("config.yaml")
    if err != nil {
        // 判断是否为文件不存在的错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("配置文件不存在,使用默认配置")
        } else {
            log.Fatal(err)
        }
    }
}
工程提示:使用 fmt.Errorf 的 %w 动词包装错误时,可形成错误链。后续调用 errors.Is 和 errors.As 可沿链查找,便于在多层调用中定位根因。