Go 错误处理详解

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 的错误处理就是"错误是值 + 显式检查 + 包装传递上下文",简单直接,没有魔法。

相关推荐
为思念酝酿的痛1 小时前
POSIX信号量
linux·运维·服务器·后端
小羊在睡觉1 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
AI玫瑰助手2 小时前
Python函数:默认参数的定义与注意事项
开发语言·python·信息可视化
油炸自行车2 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
肩上风骋2 小时前
C++14特性
开发语言·c++·c++14特性
swipe2 小时前
Neo4j + Graph RAG 医疗知识图谱工程实践:患者教育问答真正需要的是“关系可追溯”
后端·langchain·llm
源码宝3 小时前
MES系统源码:Java8 + SpringBoot2.7 + MySQL8 + Redis,后端源码清爽易扩展
java·后端·源码·springboot·mes系统·源码二开·mes源码
JAVA社区3 小时前
Java高级全套教程(十)—— SpringCloudAlibaba超详细实战详解
java·开发语言·spring cloud·面试·职场和发展
弥树子4 小时前
踩坑记录:服务器内网调用接口,真实请求URL与官方公开URL不一致问题排查
开发语言·php
金銀銅鐵4 小时前
[Java] 如何理解 class 文件中方法的 descriptor?
java·后端