首页 / Go 语言入门教程 / 错误处理策略

Go 语言入门教程

错误处理策略

本教程共 80 篇 · 第 54 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程错误处理panicerrors.Join策略Go 1.20

54. 错误处理策略

本节目标:搞懂什么时候用 error、什么时候用 panic,学会错误日志记录,理解错误忽略的代价,掌握 errors.Join 批量处理。

error vs panic

§26 详细讲了 panic 与 recover 的机制,以及「什么时候该 panic」的判断原则。这里从策略角度补充几点:

  • 能用 error 处理的就用 error,不要用 panic 代替正常的错误返回
  • panic 留给真正不该发生的情况:程序初始化失败、不可能到达的代码分支、编程错误(如传了 nil 给不允许 nil 的参数)
  • recover 主要用在边界处:HTTP handler、goroutine 入口、库的顶层调用,防止单点 panic 拖垮整个程序
  • 不确定就返回 error──这是最安全的默认选择
Note

log.Fatal 内部调用 os.Exit(1),不是 panic。效果类似(程序退出),但 panic 会执行 defer,os.Exit 不会。

错误处理策略选择

策略一:返回错误

最常用的策略。函数返回 error,调用方决定怎么处理:

func ParseConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("读取配置: %w", err)
    }
    var cfg Config
    if err := yaml.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("解析配置: %w", err)
    }
    return &cfg, nil
}

策略二:重试

对于网络错误等临时性错误,可以重试:

func FetchWithRetry(url string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        resp, err := http.Get(url)
        if err == nil {
            defer resp.Body.Close()
            return io.ReadAll(resp.Body)
        }
        lastErr = err
        time.Sleep(time.Second * time.Duration(i+1))
    }
    return nil, fmt.Errorf("重试 %d 次后仍失败: %w", maxRetries, lastErr)
}

策略三:降级

出错时用备用方案:

func GetUserName(id int) string {
    name, err := cache.Get(id)
    if err != nil {
        // 缓存失败,降级到数据库
        name, err = db.GetUserName(id)
        if err != nil {
            return "未知用户" // 最终降级
        }
    }
    return name
}

策略四:记录日志后继续

某些非关键错误可以记录后继续执行:

if err := sendMetrics(); err != nil {
    log.Printf("发送监控数据失败(不影响主流程): %v", err)
}
// 继续执行主逻辑

错误忽略的代价

忽略错误是 Go 编程中最危险的坏习惯之一:

// 坏:完全忽略错误
data, _ := os.ReadFile(path)
fmt.Println(string(data)) // 如果文件不存在,data 是 nil,打印空字符串

// 坏:忽略后用 nil 数据
user, _ := db.GetUser(id)
fmt.Println(user.Name) // 如果 GetUser 返回 nil,这里 panic
Warning

_ 忽略错误时,后续代码可能基于错误的假设运行。文件读取失败时 data 是 nil,数据库查询失败时 user 是 nil。如果不检查错误就使用这些值,很可能导致 nil 指针 panic。

errors.Join 批量错误处理

§52 介绍了 errors.Join(Go 1.20)的基本用法。它在表单验证等需要收集多个错误的场景特别好用:

package main

import (
    "errors"
    "fmt"
    "strings"
)

type FormData struct {
    Name  string
    Email string
    Age   int
}

func ValidateForm(fd FormData) error {
    var errs []error

    if fd.Name == "" {
        errs = append(errs, errors.New("名字不能为空"))
    }
    if len(fd.Name) > 50 {
        errs = append(errs, errors.New("名字太长"))
    }
    if fd.Email == "" {
        errs = append(errs, errors.New("邮箱不能为空"))
    }
    if !strings.Contains(fd.Email, "@") {
        errs = append(errs, errors.New("邮箱格式不对"))
    }
    if fd.Age < 0 || fd.Age > 150 {
        errs = append(errs, errors.New("年龄不合理"))
    }

    return errors.Join(errs...)
}

func main() {
    err := ValidateForm(FormData{
        Name:  "",
        Email: "invalid-email",
        Age:   -5,
    })

    if err != nil {
        fmt.Println(err)
        // 名字不能为空
        // 邮箱格式不对
        // 年龄不合理
    }
}

如果没有 errors.Join,你只能在第一个错误时就返回,用户一次只能看到一个错误。有了 errors.Join,可以一次性收集所有错误返回,用户体验更好。

Tip

errors.Join 传入 nil 会被自动忽略。如果所有参数都是 nil,返回 nil(表示没有错误)。

错误处理检查清单

写代码时过一遍这个清单:

  1. 是否检查了所有 error 返回值? 不要用 _ 忽略
  2. 是否添加了上下文?fmt.Errorf("...: %w", err) 包装
  3. 是否用了正确的比较方式?errors.Is 不用 ==
  4. 是否需要提取错误详情?errors.As 提取自定义类型
  5. 是否在正确的层级处理? 不要在底层 panic,不要在高层忽略
  6. 日志是否足够? 关键错误要记录日志
  7. 是否有降级方案? 非关键路径考虑降级

小结

错误处理策略的核心:

  • error 用于预期错误,panic 用于不可恢复的错误
  • 常用策略:返回错误、重试、降级、记录日志后继续
  • 永远不要忽略 error 返回值
  • errors.Join(Go 1.20)批量收集错误,提升用户体验
  • recover 用于边界防护,不是日常错误处理工具

到这里,Go 语言的核心知识就学完了。从基础语法到切片、map、指针、结构体、接口、泛型、错误处理,你已经掌握了写 Go 程序需要的全部基础。后面的章节会进入并发编程和标准库,开始写真正的 Go 程序。