写批量处理逻辑时,你八成遇到过这个纠结:校验一个表单有 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聚合多个错误的资料请关注其它相关文章!










