Day 8|error 设计与错误处理哲学(Go 工程质量的分水岭)[Go 语言 30 天系统学习计划]

学习目标
理解 Go 为什么“不用异常,而用 error”
掌握 error 的正确设计与返回方式
学会写**有语义、可判断、可传递**的错误
明确 error、panic、recover 在工程中的边界

一、为什么 Go 不使用异常(Exception)

在 Go 中:error 是普通返回值,而不是控制流机制,这是 Go 的刻意设计,而不是“能力不足”。

异常机制的典型问题

  • 控制流隐蔽(不知道哪里会抛)
  • 性能成本不可控
  • 容易被滥用
  • 调试和理解成本高

Go 的选择

  • 错误必须显式返回
  • 错误必须显式处理
  • 错误路径是“可读代码的一部分

结论:Go 宁愿你多写几行代码,也不愿你在生产环境踩雷

二、error 的本质

1. error 是一个接口

type error interface {
    Error() string
}

说明:

  • 任何实现了 Error() string 的类型,都是 error
  • error 是一个语义接口,不是字符串

2. 最简单的 error

err := errors.New("something wrong")

或:

err := fmt.Errorf("something wrong")

三、函数返回 error 的基本模式

标准函数签名

func DoSomething() (Result, error)

正确的返回顺序

return result, nil

或:

return Result{}, err

约定俗成规则:

  • error 永远放在最后一个返回值
  • 成功时 error 必须是 nil

四、不要忽略 error(Go 最重要的纪律)

错误示例(反模式)

data, _ := os.ReadFile("config.yaml")

问题:

  • 错误被直接丢弃
  • 真实问题被掩盖

正确示例

data, err := os.ReadFile("config.yaml")
if err != nil {
    return err
}

结论:

忽略 error ≈ 主动制造线上事故


五、为 error 设计“语义”,而不是字符串

1. 错误不是给人看的,是给程序判断的

错误示例:

return fmt.Errorf("user not found")

问题:

  • 只能靠字符串判断
  • 非常脆弱

2. 使用“哨兵错误(Sentinel Error)”

var ErrUserNotFound = errors.New("user not found")
if err == ErrUserNotFound {
    // 特定处理
}

优点:

  • 可比较
  • 语义明确

六、自定义 error 类型(工程必会)

1. 定义 error 类型

type PermissionError struct {
    UserID int
}

func (e *PermissionError) Error() string {
    return fmt.Sprintf("user %d has no permission", e.UserID)
}

2. 使用自定义 error

func CheckPermission(uid int) error {
    if uid != 1 {
        return &PermissionError{UserID: uid}
    }
    return nil
}
err := CheckPermission(2)
if err != nil {
    fmt.Println(err)
}

七、错误即上下文(不要“吞掉”错误)

错误示例(丢失上下文)

return errors.New("db error")

问题:

  • 原始错误信息丢失
  • 排查困难

正确示例(保留上下文)

return fmt.Errorf("query user failed: %w", err)

说明:

  • %w 表示错误包装
  • 原始错误被保留

八、panic / recover 的正确边界

1. panic 适用场景

只适用于:

  • 程序无法继续运行
  • 明显的程序错误(bug)
  • 不可恢复状态

例如:

  • nil 指针
  • 数组越界
  • 违反内部不变量

2. panic 不应该用于业务逻辑

错误示例:

if user == nil {
    panic("user not found")
}

正确做法:

if user == nil {
    return ErrUserNotFound
}

3. recover 的位置(框架层)

func SafeRun(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("panic recovered:", r)
        }
    }()
    fn()
}

结论:

recover 应该只出现在“最外层边界”


九、工程中 error 的设计层级

推荐分层

  • 底层(IO / DB):返回原始错误
  • 中层(Service):包装 + 语义
  • 上层(API / CLI):判断 + 响应

十、自检问题(进入 Day 9 前必须能回答)

  1. error 为什么是接口?
  2. 为什么不应该通过字符串判断错误?
  3. %w 解决了什么问题?
  4. panic 和 error 的边界在哪里?
  5. 什么是“错误即上下文”?

预告:Day 9

Day 9|错误包装(errors.Is / errors.As)与错误判断

你将真正学会:

  • 如何“安全判断错误”
  • errors.Is / errors.As 的原理
  • 自定义 error 与断言的配合
  • 写出“不会误判”的错误处理逻辑

发表评论