功能说明
表单校验通常希望一次性告诉用户所有问题,而不是遇到第一个错误就返回。这段代码用 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")
}
}