← 中文资源

简体中文版本

用 errors.Join 聚合字段错误

一次收集所有表单字段错误,用 errors.Join 合并并通过 errors.As 检查。

阅读英文原文

功能说明

表单校验通常希望一次性告诉用户所有问题,而不是遇到第一个错误就返回。这段代码用 errors.Join 把多个 FieldError 合并成一个错误,同时保留每个字段的独立信息,调用方可以用 errors.As 提取具体类型,也可以遍历 Unwrap() []error 得到全部错误。

它适合注册、结算、配置提交等需要逐字段提示的接口。每个错误携带字段名和规则文本,前端可以直接映射到表单控件。

关键代码

FieldError 实现 Error() 方法,返回 字段名: 规则。validate 逐项检查邮箱是否包含 @、年龄是否满 18,把失败项追加到 problems 切片,最后调用 errors.Join(problems...)。

没有错误时 errors.Join 返回 nil。多个错误时返回一个实现了 Unwrap() []error 的组合错误。main 用 errors.As 取出第一个 FieldError,再通过自定义 multiError 接口断言组合错误中包含两个子错误。

使用注意

errors.Join 的错误消息默认用换行连接,直接返回给 API 时通常需要转换成结构化的字段错误数组。errors.As 只会匹配第一个可赋值的目标,不能据此认为只有一个错误;需要全部错误时应遍历 Unwrap。

字段顺序取决于追加顺序,若前端依赖稳定顺序,应在校验前排序。错误文本不要包含用户输入中的敏感内容。对嵌套错误使用 errors.Is 判断哨兵错误,不要比较字符串。

验证方式

示例提交 Email: "nope"、Age: 15,validate 返回两个错误,errors.As 取到 email 字段错误,Unwrap 长度为 2。合法输入返回 nil,代码中的断言全部通过。可以再加入密码字段,确认子错误数量变为 3,并观察 Error() 输出的换行格式。

代码示例

package main

import (
	"errors"
	"fmt"
	"strings"
)

type FieldError struct {
	Field string
	Rule  string
}

func (err FieldError) Error() string {
	return err.Field + ": " + err.Rule
}

type Signup struct {
	Email string
	Age   int
}

type multiError interface {
	Unwrap() []error
}

func validate(signup Signup) error {
	var problems []error
	if !strings.Contains(signup.Email, "@") {
		problems = append(problems, FieldError{Field: "email", Rule: "must contain @"})
	}
	if signup.Age < 18 {
		problems = append(problems, FieldError{Field: "age", Rule: "must be at least 18"})
	}
	return errors.Join(problems...)
}

func main() {
	err := validate(Signup{Email: "nope", Age: 15})
	if err == nil {
		panic("invalid signup should fail")
	}
	fmt.Println(err)

	var fieldError FieldError
	if !errors.As(err, &fieldError) {
		panic("expected a FieldError in the joined error")
	}
	if fieldError.Field != "email" {
		panic("expected the first field error to be reported")
	}

	combined, ok := err.(multiError)
	if !ok || len(combined.Unwrap()) != 2 {
		panic("expected two joined errors")
	}
	if validate(Signup{Email: "[email protected]", Age: 30}) != nil {
		panic("valid signup should pass")
	}
}