写批量处理逻辑时,你八成遇到过这个纠结:校验一个表单有 10 个字段,第一个字段不合法就 return err,用户改完提交发现第二个又错了,来回折腾五六次。你想「一次把所有错误都告诉他」,于是把错误拼成字符串:

var msgs []string
if name == "" {
    msgs = append(msgs, "name 不能为空")
}
if age < 0 {
    msgs = append(msgs, "age 不能为负")
}
if len(msgs) > 0 {
    return errors.New(strings.Join(msgs, "; ")) // 拼成一个字符串
}

能跑,但有个硬伤:拼成字符串后,错误类型全丢了。上层拿到的是一个普通 errors.New,没法用 errors.Is 判断里面有没有某个特定的 sentinel error,也没法用 errors.As 取出某个具体类型。Go 1.20 加的 errors.Join 就是来解决这个的。

errors.Join 基础:把多个 error 装进一个

errors.Join 接收任意个 error,返回一个「包含它们全部」的错误。nil 参数会被自动忽略,如果全是 nil 就返回 nil:

package main

import (
    "errors"
    "fmt"
)

func validate(name string, age int) error {
    var errs []error // 收集错误,而不是遇到第一个就返回
    if name == "" {
        errs = append(errs, errors.New("name 不能为空"))
    }
    if age < 0 {
        errs = append(errs, errors.New("age 不能为负"))
    }
    if age > 150 {
        errs = append(errs, errors.New("age 超出合理范围"))
    }
    return errors.Join(errs...) // 全 nil 时返回 nil,天然处理「无错误」
}

func main() {
    err := validate("", -5)
    if err != nil {
        fmt.Println(err)
    }
}

输出:

name 不能为空
age 不能为负

注意两个细节:第一,errors.Join 的默认输出是每个错误占一行(用 \n 连接),不是逗号分隔;第二,errs 里就算混入 nil 也没关系,Join 会跳过——所以你可以放心地 append,不用先判空。全部为 nil 时返回值就是 nil,if err != nil 自然成立,这个「零错误即成功」的语义帮你省掉手动计数。

关键优势:errors.Is / errors.As 能穿透进去

这才是 errors.Join 比字符串拼接强的地方。聚合后的错误,依然能被 errors.Is 和 errors.As 识别:

package main

import (
    "errors"
    "fmt"
)

// 定义两个 sentinel error
var (
    ErrEmptyName = errors.New("name 为空")
    ErrBadAge    = errors.New("age 非法")
)

func validate(name string, age int) error {
    var errs []error
    if name == "" {
        errs = append(errs, ErrEmptyName)
    }
    if age < 0 {
        errs = append(errs, ErrBadAge)
    }
    return errors.Join(errs...)
}

func main() {
    err := validate("", -1)

    // 聚合错误里包含 ErrEmptyName 吗?能判出来
    if errors.Is(err, ErrEmptyName) {
        fmt.Println("命中:name 为空") // 会打印
    }
    if errors.Is(err, ErrBadAge) {
        fmt.Println("命中:age 非法") // 也会打印
    }
}

两条都会命中。errors.Join 返回的对象内部实现了一个 Unwrap() []error 方法(注意是返回切片,和传统的 Unwrap() error 不同),errors.Is 遍历这个切片逐个比对,所以聚合里的任何一个 sentinel 都能被查到。字符串拼接方案彻底做不到这一点——这就是「保留类型信息」的价值。

errors.As 同理,能从聚合里把某个具体类型的错误取出来:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "字段校验失败: " + e.Field
}

func main() {
    err := errors.Join(
        errors.New("普通错误"),
        &ValidationError{Field: "email"},
    )

    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Println("拿到具体字段:", ve.Field) // 拿到具体字段: email
    }
}

实战:并发子任务收集错误

批量校验只是热身。errors.Join 在并发场景更能打——多个 goroutine 各干各的,你想把所有失败的都收起来一起返回,而不是只报第一个:

package main

import (
    "errors"
    "fmt"
    "sync"
)

func processItem(id int) error {
    if id%3 == 0 {
        return fmt.Errorf("item %d 处理失败", id)
    }
    return nil
}

func processAll(ids []int) error {
    var (
        mu   sync.Mutex // errs 会被多个 goroutine 写,必须加锁
        errs []error
        wg   sync.WaitGroup
    )

    for _, id := range ids {
        wg.Add(1)
        go func(id int) { // Go 1.22 前循环变量要显式传参,别踩闭包坑
            defer wg.Done()
            if err := processItem(id); err != nil {
                mu.Lock()
                errs = append(errs, err) // 并发 append 必须锁
                mu.Unlock()
            }
        }(id)
    }

    wg.Wait()
    return errors.Join(errs...) // 全部成功则返回 nil
}

func main() {
    err := processAll([]int{1, 2, 3, 4, 5, 6})
    if err != nil {
        fmt.Println("有任务失败:")
        fmt.Println(err)
    }
}

输出(顺序可能不同,因为并发):

有任务失败:
item 3 处理失败
item 6 处理失败

有两个点必须记牢:一是 errs 被多个 goroutine 并发写,append 不是并发安全的,必须加 mu.Lock(),否则 -race 一跑就报数据竞争,甚至丢数据;二是这里没用 errgroup,因为 errgroup 默认只保留第一个返回的错误,而我们要的是「全都收上来」——这正是 errors.Join 的用武之地,两者定位不同,别混用。

一个容易忽略的坑:fmt.Errorf 只能包一个,%w 别写多个的旧认知已过时

Go 1.20 顺带给 fmt.Errorf 也开了多 %w 的能力,这点很多老文章没更新:

err1 := errors.New("底层错误 A")
err2 := errors.New("底层错误 B")

// Go 1.20 起,一个 Errorf 里可以有多个 %w
wrapped := fmt.Errorf("操作失败: %w, 以及 %w", err1, err2)

fmt.Println(errors.Is(wrapped, err1)) // true
fmt.Println(errors.Is(wrapped, err2)) // true

也就是说 fmt.Errorf 现在既能包装、又能聚合(多个 %w)。那什么时候用 Errorf 什么时候用 Join?要给错误加一段上下文描述、错误数量固定,用 fmt.Errorf("...: %w, %w", ...);错误数量动态(来自切片/循环)、只想纯聚合不加文案,用 errors.Join(errs...)。后者传切片更自然,前者格式化字符串更灵活。

小结

  • errors.Join(errs...) 把多个错误合成一个,自动忽略 nil、全 nil 返回 nil,默认按行拼接输出。
  • 相比字符串拼接,它保留每个错误的类型,errors.Is / errors.As 能穿透聚合逐个匹配——这是它存在的核心理由。
  • 底层靠 Unwrap() []error(返回切片)实现,和传统 Unwrap() error 不同。
  • 并发收集错误时,共享的 errs 切片 append 必须加锁;要「收集全部错误」用 Join,要「只保留第一个错误 + 自动取消」用 errgroup,别搞混。
  • Go 1.20 起 fmt.Errorf 支持多个 %w,数量固定且要加文案时用它,动态数量纯聚合时用 Join。
  • 一句话记忆点:遇到「一次性把多个错误都返回、还要能被上层精确识别」的需求,errors.Join 就是标准答案。

以上就是Go语言使用errors.Join聚合多个错误的操作指南的详细内容,更多关于Go errors.Join聚合多个错误的资料请关注其它相关文章!

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章