第 7 章:错误处理与 panic/recover

jerry北京市2026年5月8日Go 21 次阅读 约 10 分钟
第 7 章:错误处理与 panic/recover

掌握 Go 独特的错误处理哲学、自定义错误、错误包装链,以及 panic/recover 机制。


7.1 error 接口

Go 没有异常机制(try/catch),而是通过返回值处理错误:

// error 是一个内置接口
type error interface {
    Error() string
}

// 标准用法
f, err := os.Open("file.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

这种 if err != nil 的模式在 Go 中无处不在,是 Go 的核心设计哲学之一:错误是值,应该被显式处理。

7.2 创建错误

import (
    "errors"
    "fmt"
)

// 方式一:errors.New
err1 := errors.New("something went wrong")

// 方式二:fmt.Errorf(支持格式化)
name := "config.yaml"
err2 := fmt.Errorf("无法读取文件: %s", name)

7.3 自定义错误类型

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 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "年龄必须在 0-150 之间",
        }
    }
    return nil
}

// 使用
if err := validateAge(-1); err != nil {
    // 类型断言获取详细信息
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
    }
}

7.4 错误包装(Go 1.13+)

// 包装错误:使用 %w
originalErr := errors.New("数据库连接失败")
wrappedErr := fmt.Errorf("用户查询失败: %w", originalErr)

fmt.Println(wrappedErr)
// 输出:用户查询失败: 数据库连接失败

// 解包:errors.Unwrap
inner := errors.Unwrap(wrappedErr)
fmt.Println(inner)  // 数据库连接失败

errors.Iserrors.As

var ErrNotFound = errors.New("not found")

func findUser(id int) error {
    return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
}

err := findUser(42)

// errors.Is:判断错误链中是否包含特定错误
if errors.Is(err, ErrNotFound) {
    fmt.Println("用户不存在")
}

// errors.As:从错误链中提取特定类型的错误
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println(ve.Field)
}

errors.Is vs ==

  • == 只比较当前层
  • errors.Is 会遍历整个错误链

7.5 错误处理最佳实践

// 1. 尽早返回,减少嵌套
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("打开文件失败: %w", err)
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return fmt.Errorf("读取文件失败: %w", err)
    }

    // 处理 data...
    return nil
}

// 2. 定义哨兵错误(Sentinel Error)
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInternal     = errors.New("internal error")
)

// 3. 添加上下文信息
func getUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(id=%d): %w", id, err)
    }
    return user, nil
}

7.6 panic 和 recover

panic 用于不可恢复的错误,recover 用于捕获 panic:

// panic 会终止当前函数,逐层向上传播
func mustPositive(n int) {
    if n <= 0 {
        panic("必须是正数")
    }
}

// recover 只能在 defer 中使用
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    return a / b, nil
}

result, err := safeDiv(10, 0)
if err != nil {
    fmt.Println(err)  // panic recovered: runtime error: integer divide by zero
}

什么时候用 panic

  • 程序初始化阶段的致命错误(配置缺失、数据库连接失败等)
  • 不应该发生的逻辑错误(程序 bug)
  • 标准库中的 Must 系列函数
// 标准库示例
template.Must(template.New("").Parse("{{.Name}}"))
regexp.MustCompile(`\d+`)

不应该用 panic 的场景:

  • 可预期的错误(文件不存在、网络超时等)
  • 用户输入验证
  • 业务逻辑错误

7.7 面试要点

  1. Go 为什么不用 try/catch?

    • 错误是值,应该被显式处理
    • 避免异常被忽略或滥用
    • 代码流程更清晰
  2. errors.Iserrors.As 的区别?

    • Is 判断错误链中是否包含某个特定错误值
    • As 从错误链中提取某个特定类型的错误
  3. %w%v 包装错误的区别?

    • %w 保留错误链,可以用 errors.Is/As 解包
    • %v 只是字符串拼接,丢失了原始错误
  4. panic 和 error 的使用场景?

    • error:可预期的错误,正常业务流程
    • panic:不可恢复的致命错误,程序 bug

练习

  1. 实现一个自定义错误类型 HTTPError,包含状态码和消息
  2. 编写一个函数链,每层添加上下文信息,最后用 errors.Is 判断根因
  3. 使用 recover 实现一个安全的函数执行器
  4. 实现一个 Must 函数,将 (T, error) 转换为 T(error 时 panic)

← 上一章:接口与多态 | 下一章:包管理与项目组织 →

评论

登录 后发表评论

暂无评论