Go error 处理:errors.Is/As 与错误包装

Go error 处理:errors.Is/As 与错误包装

Go 的错误处理一直被吐槽啰嗦,但真正让人头疼的不是 if err != nil 写得多,而是错误传到上层就"失真"了 。你在最底层返回了 sql.ErrNoRows,一路 return err 传上来,到 handler 里想判断"是不是记录不存在",结果发现要么信息丢了,要么只能用 err.Error() == "..." 这种字符串比较------脆得一改文案就崩。

Go 1.13 引入的错误包装(wrapping)和 errors.Is / errors.As 就是来解决这个的。这篇讲清楚:怎么包装错误既保留上下文又不丢原始类型,以及怎么在上层正确地判断和提取错误。

朴素写法的两个坑

先看没有包装时的常见做法。坑一:直接 return err,上层拿到错误但不知道它是在哪一步出的

go 复制代码
func loadUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name); err != nil {
        return nil, err // 只返回底层错误,丢了"loadUser 这一层"的上下文
    }
    return &u, nil
}

坑二:为了加上下文,用 fmt.Errorf 拼字符串,结果原始错误类型被抹掉了:

go 复制代码
if err := row.Scan(&u.Name); err != nil {
    // %v 只是把错误转成字符串塞进去,sql.ErrNoRows 这个"身份"没了
    return nil, fmt.Errorf("loadUser id=%d: %v", id, err)
}

上层想判断"是不是没查到记录",只能被迫写:

go 复制代码
if strings.Contains(err.Error(), "no rows") { // 脆弱:依赖错误文案,库一改就废
    // ...
}

字符串匹配是错误处理里最该避免的反模式。

用 %w 包装:既加上下文又保留原始错误

fmt.Errorf%w 动词(w = wrap)是关键。它在字符串里嵌入上下文的同时,把原始错误"链接"进新错误里,原始类型不丢:

go 复制代码
func loadUser(id int) (*User, error) {
    row := db.QueryRow("SELECT name FROM users WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name); err != nil {
        // 用 %w 而不是 %v:错误链里仍能找到 sql.ErrNoRows
        return nil, fmt.Errorf("loadUser id=%d: %w", id, err)
    }
    return &u, nil
}

%v%w 打印出来的文字一模一样,区别全在能不能被 errors.Is/As 识别 。记住一条规则:给上层留了判断需要的错误,就用 %w;纯粹只想记个日志、不需要上层再识别,用 %v

errors.Is:判断"是不是某个特定错误"

包装之后,上层用 errors.Is 沿着错误链查找,不管中间包了多少层:

go 复制代码
u, err := loadUser(42)
if err != nil {
    // errors.Is 会顺着 %w 链一路找,底层的 sql.ErrNoRows 照样能命中
    if errors.Is(err, sql.ErrNoRows) {
        http.Error(w, "用户不存在", http.StatusNotFound)
        return
    }
    // 其他错误当 500
    http.Error(w, "服务器错误", http.StatusInternalServerError)
    return
}

errors.Is(err, target) 的语义是"err 的错误链里,是否存在等于 target 的错误"。这就是它取代 err == sql.ErrNoRows 的原因------直接 == 在错误被包装后会失败,errors.Is 不会。

自定义哨兵错误(sentinel error)也用同一套:

go 复制代码
// 用 errors.New 定义包级别的哨兵错误,导出给调用方判断
var ErrUserBanned = errors.New("user is banned")

func checkUser(u *User) error {
    if u.Banned {
        return fmt.Errorf("checkUser %s: %w", u.Name, ErrUserBanned)
    }
    return nil
}

// 调用方
if errors.Is(err, ErrUserBanned) {
    // 精确处理"被封禁"这一种情况
}

errors.As:提取"某种类型的错误"拿它的字段

有时候光知道"是哪种错误"不够,你还想拿到错误里携带的数据 (比如出错的字段名、HTTP 状态码)。这时用 errors.As,它把错误链里匹配类型的那个错误"取出来"赋值给你的变量:

go 复制代码
// 一个携带额外信息的自定义错误类型
type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Msg)
}

func validate(u *User) error {
    if u.Age < 0 {
        return &ValidationError{Field: "age", Msg: "不能为负数"}
    }
    return nil
}

上层用 errors.As 把它捞出来,读它的字段:

go 复制代码
err := validate(u)

var vErr *ValidationError
// errors.As 找到链里第一个 *ValidationError,赋值给 vErr,返回 true
if errors.As(err, &vErr) {
    // 现在能拿到具体是哪个字段错了
    fmt.Printf("请检查字段: %s (%s)\n", vErr.Field, vErr.Msg)
}

注意 errors.As 的第二个参数是指向目标变量的指针 (这里 vErr 本身是 *ValidationError,所以传 &vErr)。这是新手最容易搞错的地方,传错编译期就会 panic 提示你。

Is 还是 As?一句话区分

  • errors.Is :判断"是不是这一个/这一种已知错误",拿来做分支。目标是一个具体的错误值(哨兵错误)。
  • errors.As :判断"链里有没有某种类型 的错误",并且要取出它携带的字段。目标是一个类型。

简记:只要判断身份用 Is,还想读数据用 As。

一个容易忽略的细节:别过度包装

包装很好用,但不是每层都要包。原则是:每一层加的上下文要有信息增量。下面这种层层复读是噪声:

go 复制代码
// 反面例子:每层都包一遍,错误信息变成 "a: b: c: d: 真正的错误"
return fmt.Errorf("service call: %w", err)      // 上层
return fmt.Errorf("repo query: %w", err)         // 中层
return fmt.Errorf("db exec: %w", err)            // 底层

实践建议:在跨越模块边界 或**能补充关键上下文(如 id、操作名)**时包装一次,中间纯透传的地方直接 return err 即可。

小结

  • fmt.Errorf%w 包装错误:既加上下文,又让原始错误留在链里可被识别;%v 会抹掉类型,只适合记日志。
  • errors.Is(err, target) 沿错误链判断"是不是某个特定错误",取代脆弱的 == 和字符串匹配。
  • errors.As(err, &target) 从链里提取某种类型的错误,拿来读它携带的字段;第二个参数要传指针。
  • 判断身份用 Is,要读数据用 As;包装要有信息增量,别层层复读。
  • 一句话记忆:%w 包上下文,用 Is/As 拆链判断------错误传到天边,身份也不会丢。
相关推荐
红糖奶茶1 小时前
Python 中 while 循环计数异常的常见原因分析与正确搓搓
开发语言·python
Lucky_Turtle1 小时前
【论文写作】PDF图片不清晰,打印生成PDF空白大,PDF字体嵌入
开发语言·pdf·c#
csdndenglu1 小时前
Easy3D:一个轻量级、易用、高效的C++库,用于处理和渲染3D数据,无复杂要求时可替代VTK
开发语言·c++·图形渲染·3d渲染
doiito1 小时前
【安全,架构】RustyVault 项目深度分析报告以及和HashiCorp Vault的差异
后端·安全·rust
尚早立志2 小时前
Spring Boot 之 BeanDefinitionLoader 详解
java·spring boot·后端
byte轻骑兵2 小时前
【AVRCP】规范精讲[41]:AV/C指令帧解析,蓝牙遥控交互的底层语言
c语言·开发语言·人机交互·智能制造·avrcp
发光小北2 小时前
Bronze100系列plc Modbus及自由口通信案例
开发语言·php
玉宇夕落2 小时前
Node.js 路径模块(path)的学习
后端
snow@li2 小时前
Java:后端项目会有一个类似前端node_modules的目录吗 / jar包在本地仓库 ~/.m2/repository 中按版本共存
java·开发语言·前端