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

相关推荐
dialect_5 小时前
KMP算法
开发语言·c++·算法
总要学点什么6 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
深蓝AI6 小时前
Python 3.13 的 Queue.shutdown:异步消费者终于能正常收工了
后端
武子康7 小时前
MultiNet6 空闲崩溃:`EXCVADDR=0x8` 后又出现 `CORRUPT HEAP`
人工智能·后端·agent
开开心心_Every7 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
chenbingjie_c7 小时前
C++ 多态完全指南:从虚函数重写到虚函数表原理
开发语言·c++
吴卫斌8 小时前
定投ETF实战指南(四):行业/主题ETF——定投的“进攻端“
开发语言·python·股票·量化
武子康8 小时前
MultiNet7 每轮都 clean,ESP32-P4 内存为什么还在掉?
人工智能·后端·agent
超级架构师9 小时前
PRAXO-CORE 2.5.0:用 conformance 和显式边界构建 Go Agent 框架
开发语言·人工智能·后端·golang·ai编程·智能体框架·praxo-core
陈随易9 小时前
AI 运维服务器,安全又可靠的方法
前端·后端·程序员