Go 错误处理详解
核心原则
Go 的错误处理哲学:错误是值,不是异常。没有 try/catch,而是通过返回值显式传递错误,强制调用方处理。
1. error 接口
go
type error interface {
Error() string
}
所有错误都实现这个接口。内置的 errors.New() 和 fmt.Errorf() 是最常用的创建方式:
go
err := errors.New("文件不存在")
err := fmt.Errorf("打开文件失败: %w", err) // %w 包装错误,支持 Unwrap
2. 三种惯用模式
模式一:直接检查(最常见)
go
f, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("打开文件: %w", err)
}
defer f.Close()
模式二:哨兵错误(Sentinel Errors)
go
var ErrNotFound = errors.New("未找到")
func Find(id int) (*Item, error) {
// ...
if notFound {
return nil, ErrNotFound
}
return item, nil
}
// 调用方用 errors.Is 判断
item, err := Find(1)
if errors.Is(err, ErrNotFound) {
// 专门处理"未找到"
}
⚠️ 用
errors.Is()而非==,因为被包装后==会失效。
模式三:自定义错误类型
go
type NotFoundError struct {
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("ID %d 未找到", e.ID)
}
// 调用方用 errors.As 提取具体类型
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("未找到的 ID: %d\n", nfe.ID)
}
3. 错误包装(Error Wrapping)--- Go 1.13+
go
// fmt.Errorf + %w 创建包装链
return fmt.Errorf("数据库查询失败: %w", dbErr)
// errors.Is --- 沿包装链检查是否有某个错误
errors.Is(err, sql.ErrNoRows) // true,即使被包装了
// errors.As --- 沿包装链提取某个类型的错误
var pqErr *pq.Error
errors.As(err, &pqErr)
包装 vs 拼接的区别:
go
fmt.Errorf("查询失败: %w", err) // 包装,errors.Is/As 可穿透
fmt.Errorf("查询失败: %v", err) // 拼接,信息丢失,无法 Unwrap
4. panic / recover --- 仅用于真正的异常
使用场景极窄:程序逻辑上不可能恢复的错误(如数组越界、nil 指针、初始化失败)。
go
// panic:程序无法继续
func init() {
if config == nil {
panic("配置不能为空") // 合理:启动时致命错误
}
}
// recover:捕获 panic(仅在 defer 中有效)
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("内部错误: %v", r)
}
}()
return a / b, nil // b=0 时 panic 被 recover 捕获
}
原则:包的公共 API 不应该 panic,让调用方决定如何处理。
5. 错误处理的最佳实践
| 做法 | 说明 |
|---|---|
| ✅ 尽早返回 | if err != nil { return err },减少嵌套 |
| ✅ 添加上下文 | 每层包装时加上当前操作的信息 |
✅ 用 %w 包装 |
保留错误链,方便上层诊断 |
| ✅ 导出哨兵错误 | 用 var ErrXxx = errors.New(...) |
| ❌ 不要忽略错误 | _ = DoSomething() 几乎总是错的 |
| ❌ 不要用 panic 做流程控制 | 不同于 Java 的 checked exception |
| ❌ 不要在循环里重复创建错误 | 用 var ErrXxx 在包级别声明 |
6. Go 1.13 vs 之前对比
Go 1.12 及之前: if err == ErrNotFound // 包装后失效
Go 1.13+: if errors.Is(err, ErrNotFound) // 穿透包装链
这是 Go 错误处理最大的演进,核心就是 errors.Is / errors.As + %w 包装。
7. 实战示例:分层错误处理
go
// Repository 层
var ErrUserNotFound = errors.New("用户不存在")
func (r *UserRepo) GetByID(ctx context.Context, id int) (*User, error) {
row := r.db.QueryRowContext(ctx, "SELECT ... WHERE id=$1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user repo get by id %d: %w", id, ErrUserNotFound)
}
return nil, fmt.Errorf("user repo get by id %d: %w", id, err)
}
return &u, nil
}
// Service 层 --- 用 errors.Is 判断业务错误
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
u, err := s.repo.GetByID(ctx, id)
if errors.Is(err, ErrUserNotFound) {
return nil, ErrUserNotFound // 透传业务错误
}
if err != nil {
return nil, fmt.Errorf("user service get: %w", err)
}
return u, nil
}
// HTTP 层 --- 根据错误类型返回不同状态码
if errors.Is(err, ErrUserNotFound) {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
一句话总结:Go 的错误处理就是"错误是值 + 显式检查 + 包装传递上下文",简单直接,没有魔法。