学习目标
理解 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 前必须能回答)
- error 为什么是接口?
- 为什么不应该通过字符串判断错误?
- %w 解决了什么问题?
- panic 和 error 的边界在哪里?
- 什么是“错误即上下文”?
预告:Day 9
Day 9|错误包装(errors.Is / errors.As)与错误判断
你将真正学会:
- 如何“安全判断错误”
- errors.Is / errors.As 的原理
- 自定义 error 与断言的配合
- 写出“不会误判”的错误处理逻辑