你刚修好邮箱格式,提交。系统报错"密码太短"。修完密码,提交。又报"国家代码无效"。再修,再报"必须同意条款"。四个来回,每次只看到一个错误——因为你的函数遇到第一个错误就返回,后面的校验根本没跑。
这是Go 1.20引入errors.Join之前,每个写校验逻辑的人都踩过的坑。三年过去,大部分代码还在用if err != nil { return err }的老习惯。这个习惯大多数时候是对的,但在三种特定场景下是错的。分辨你在哪种场景,就是这门手艺的全部。
![]()
聚合错误长什么样
errors.Join(err1, err2, err3)把多个非空错误包成一个。它的Error()方法会把每个被包的错误单独输出一行。errors.Is和errors.As会遍历整棵树,不只是第一个分支。如果所有参数都是nil,它返回nil——这个特性让聚合模式的代码读起来很干净。
三行校验,一次调用,一个分支。两个失败你会看到两个。全过就继续。
1.20之前怎么干?用github.com/hashicorp/go-multierror:
同一个思路,多两行代码,多一个依赖,外加一个开箱不支持errors.Is的自定义类型。multierror在它那个年代是好答案。现在标准库才是。
校验场景只是开胃菜
真正有意思的是扇出RPC。一个请求要调四个下游服务来拼装响应。两个挂了。用户拿到500,提示"服务B不可用",你接下来一小时都在排查B,最后才发现D也挂了,原因还不一样。
用errors.Join聚合四个服务的错误,调用方一眼看到全部故障点。调试时间从一小时缩到一分钟。
代码里用sync.WaitGroup并行拉取,sync.Mutex保护错误切片。每个goroutine把错误带标签包一层再塞进去。最后errors.Join(errs...)统一返回。
什么时候不该聚合
错误聚合不是万能药。三种情况要慎用:
第一,调用方只关心"成没成",不关心"怎么没的"。HTTP 404不需要附带文件系统底层错误,用户看不懂,日志里已经记了。
第二,错误有明确的优先级顺序。数据库连接失败比查询语法错误更严重,前者该直接中断,不该混在一起让调用方猜哪个是根因。
第三,错误数量可能爆炸。循环里每个元素都报错,聚合成百上千条,Error()字符串直接撑爆日志缓冲区。这时候该用结构化日志单独记,或者采样。
一个判断标准
问自己:调用方修复一个错误后,是否值得再次尝试?校验场景里,用户改完密码就该重新提交,值得。RPC场景里,运维修好服务B后该自动重试,也值得。
如果答案是"不值得"——比如文件不存在就是不存在,重试也没用——那就用传统的错误链,第一时间把根因抛上去。
Go的错误处理被吐槽多年,但errors.Join其实给了一个精妙的区分:错误是"流程中断信号"还是"待办事项清单"。前者用链式包裹,后者用扁平聚合。混用这两种模式,代码会自己告诉你哪里别扭。
最后说句得罪人的话:还在用go-multierror的新项目,要么维护者没看1.20的release note,要么看了觉得"迁移成本太高"——但这两个函数的行为几乎一致,替换就是改个import的事。不迁,大概率和当年坚持用dep不用go mod的是同一批人。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.