尧图精选

深入解析 cockroachdb/errors:面向分布式系统的 Go 网络可移植错误处理库

🕒 发布时间:2026/9/17 18:24:36 📁 来源:尧图网络
深入解析 cockroachdb/errors面向分布式系统的 Go 网络可移植错误处理库【免费下载链接】inngestThe leading workflow orchestration platform. Run stateful step functions and AI workflows on serverless, servers, or the edge.项目地址: https://gitcode.com/GitHub_Trending/in/inngest导读cockroachdb/errors是一个定位于即插即用的 Go 错误处理库目标是在不改变既有代码习惯的前提下作为 Go 标准库errors与github.com/pkg/errors的直接替代品并额外提供错误对象的网络可移植性——这是面向分布式系统、多版本软件混跑场景的核心能力。本篇将基于本仓库 vendor 目录中的官方 READMEvendor/github.com/cockroachdb/errors/README.md结合库源码逐一讲解其特性矩阵、错误叶子与包装器构造、PII 脱敏机制、Sentry 上报、protobuf 编解码原理以及自定义错误类型的接入方式。读完本文你将掌握如何用一套 API 同时获得标准错误链、跨进程错误识别、无 PII 报告字符串和自动化的 Sentry 事件构建能力。一、库定位替代、兼容与网络可移植性该库的原始设计来自 CockroachDB 团队其明确目标是同时满足三类使用场景替代github.com/pkg/errors提供同名的New、Wrap、Cause等 API存量代码可平滑迁移兼容 Go 标准库errors提供Is、As、Unwrap等语义满足 Go 1.13 的错误链约定提供网络可移植性错误对象可以编码为 protobuf 并在进程/主机间传输接收方即使不认识发送方新增的错误类型也能正确保留并转发这就是文档中所说的前向兼容forward compatibility。本仓库将其以v1.11.3版本作为间接依赖引入见 go.mod 第 151 行这也说明它常被用作基础错误设施被上层业务间接消费。特性矩阵对比原 README 给出了一张横向对比表这里完整保留并加以注释特性Go 1.13errorspkg/errorsGo 1.13errors/xerrorscockroachdb/errors错误构造器New、Errorf等✔✔✔✔错误原因Cause/Unwrap✔✔✔原因屏障Opaque/Handled✔✔errors.As()、errors.Is()✔✔格式串以: %w结尾时自动包装✔✔带高效栈捕获的标准包装器✔✔透明的 protobuf 编解码与前向兼容✔errors.Is()跨网络识别错误✔对无 PII 可报告字符串的全面支持✔同时支持Cause()与Unwrap()go#31778✔向 Sentry.io 上报标准错误报告✔断言失败的包装器✔带 issue 跟踪链接的包装器✔面向用户的提示与详情包装器✔附加次要原因的包装器✔附加context.Context中logtags信息的包装器✔errors.FormatError()、Formatter、Printer建设中✔errors.SafeFormatError()、SafeFormatter✔感知包装的IsPermission()、IsTimeout()、IsExist()、IsNotExist()✔其中前向兼容的含义是当一个软件包的更新版本向运行旧版本的另一个系统发送新错误对象时旧系统仍能正确识别并妥善处理这些它从未见过的错误类型。二、核心概念错误叶子Leaf与包装器Wrapper在深入 API 之前必须先厘清库的两个基本概念原文定义错误叶子leaf实现了error接口但不通过Unwrap()或Cause()引用其他错误的对象错误包装器wrapper实现了error接口并同时通过Unwrap()首选和/或Cause()引用另一个错误的对象。一个重要的设计细节所有包装器构造器对nil错误都是安全的no-op。这意味着你可以把防御性判断简化为一行代码// 无需写成 // if err : foo(); err ! nil { // return errors.Wrap(err, foo) // } // return nil // // 直接 return errors.Wrap(foo(), foo)三、可用的错误叶子构造器原文档将叶子构造器与使用建议一一对应此处完整列出New(string) error、Newf(string, ...interface{}) error、Errorf(...)带消息的叶子错误。适用场景常见错误。行为在调用点捕获栈轨迹并对消息做脱敏处理以便安全上报。细节访问Error()与常规 Go 格式化Sentry 报告中包含细节。变体errors.NewWithDepth()系列可自定义栈轨迹捕获的调用深度。AssertionFailedf(string, ...interface{}) error、NewAssertionFailureWithWrappedErrf(error, string, ...interface{}) error标识断言失败/编程错误。适用场景不变量被破坏、进入不可达代码路径。行为在调用点捕获栈、脱敏传入字符串并准备一条面向人类的提示。细节访问IsAssertionFailure()/HasAssertionFailure()%v格式化Sentry 报告包含安全细节。变体errors.AssertionFailedWithDepthf()。从源码看AssertionFailedf实际是NewWithDepthf叠加assert.WithAssertionFailure的结果见 errutil/assertions.go印证了组合优于继承的实现思路。Handled(error) error、Opaque(error) error、HandledWithMessage(error, string) error捕获错误原因但使其对Unwrap()/Is()不可见屏障。适用场景处理错误的过程中产生了新错误而原始错误需要被隐藏。行为原因被保存在隐藏字段中消息默认保留除非使用...WithMessage()变体。细节访问%v格式化Sentry 报告中以脱敏形式递归呈现。UnimplementedError(IssueLink, string) error捕获消息字符串与一个外部资源 URL标识尚未实现的功能。适用场景告知人类用户某功能尚未实现并引导其访问外部资源。行为URL 与细节被视为安全内容可上报。细节访问errors.GetAllHints()、errors.FlattenHints()、%vSentry 报告包含 URL 与细节不含消息本身。四、可用的包装器构造器原文档对每个包装器都给出了何时用、做什么、怎么看细节三要素下面完整继承并补充源码佐证4.1Wrap/Wrapf错误返回路径的首选适用场景错误返回路径上。行为等价于WithMessage()WithStack()WithSafeDetails()的组合。细节访问Error()、常规格式化、Sentry 报告。变体WrapWithDepth()系列。4.2WithSecondaryError与CombineErrors次要错误与并发合并WithSecondaryError(error, error)为主错误附加一个次要错误但对errors.Is()隐藏。适用于处理主错误时又发生了附加错误的场景。CombineErrors()则用于并发场景两个操作可分别失败、最终只返回一个错误——它在一个参数为nil时直接返回另一个否则调用WithSecondaryError()。4.3Mark错误身份的显式覆盖当调用方期望用errors.Is()识别某个哨兵错误但被调方返回的错误消息五花八门时Mark(err, refErr)会把err的错误标记error mark覆盖为refErr的标记。这与errors.Is()的实现密切相关markers.Is在完成直接引用比较与委托Is()方法之后会退而求其次比较错误标记见 markers/markers.go。这也是跨网络仍能识别错误的机制基础——标记随编码在网络间保留。4.4WithStack显式栈捕获通常无需单独使用用Wrap即可但有两个典型特例返回哨兵错误时例如var myErr errors.New(foo) func myFunc() error { if ... { return errors.WithStack(myErr) } }错误返回路径上不值得包装但需要上下文时err : foo() if err ! nil { doSomething() if !somecond { return errors.WithStack(err) } }栈轨迹被认为对上报安全可通过%v、errors.GetSafeDetails()与 Sentry 报告获取。变体WithStackDepth()。4.5 消息与上下文类包装器WithMessage/WithMessagef添加消息前缀Error()、常规格式化与 Sentry 报告可见。WithSafeDetails保存用于安全上报的字符串一般建议直接用Wrap。WithDetail/WithDetailf面向开发者的上下文详情通过errors.GetAllDetails()、errors.FlattenDetails()获取%v可见但不进入 Sentry 报告。WithHint/WithHintf面向终端用户的行动建议通过errors.GetAllHints()、errors.FlattenHints()获取提示会去重%v可见不进入 Sentry 报告。4.6 可观测性与治理类包装器WithIssueLink(error, IssueLink)附加 URL 与任意字符串两者均被视为安全内容UnimplementedError()是其叶子形态的对应物。WithTelemetry(error, string)附加遥测键供服务端遥测子系统采集键被视为安全内容可通过errors.GetTelemetryKeys()获取。WithDomain(error, Domain)、HandledInDomain(...)、HandledInDomainWithMessage(...)实验性在包边界标注错误来源域可用errors.EnsureNotInDomain()、errors.NotInDomain()断言。WithAssertionFailure(error)把错误标注为断言失败一般直接用AssertionFailedf系列会触发自动生成的提示。WithContextTags(error, context.Context)捕获context.Context中由logtags挂载的 k/v 对可通过errors.GetContextTags()读取。五、错误对象会输出什么格式化与可见性矩阵同一个错误对象在不同输出通道下可见的内容并不相同。原文档给出了权威对照表此处完整保留错误细节Error()与%s/%q/%v%vGetSafeDetails()ReportError()Sentry 报告主消息如New()可见可见是v1.6 起完整v1.6 起包装前缀如WithMessage()可见作为前缀可见是v1.6 起完整v1.6 起栈轨迹如WithStack()不可见简化形式是完整提示如WithHint()不可见可见否仅类型详情如WithDetail()不可见可见否仅类型断言失败标注如WithAssertionFailure()不可见可见否仅类型issue 链接如WithIssueLink()、UnimplementedError()不可见可见是完整安全细节如WithSafeDetails()不可见不可见是完整遥测键如WithTelemetryKey()不可见可见是完整次要错误如WithSecondaryError()、CombineErrors()不可见可见脱敏递归脱敏递归屏障来源如Handled()不可见可见脱敏递归脱敏递归错误域如WithDomain()不可见可见是完整上下文标签如WithContextTags()不可见可见键可见值脱敏键可见值脱敏这张表是理解为什么生产事故报告里看不到某些字段的关键只有被显式标记为安全的内容才会进入GetSafeDetails()与 Sentry 报告。六、PII 脱敏如何提供无个人信息的细节库对 PII 的处理策略是默认脱敏 显式选入默认情况下错误对象中的大量字符串被视为PII 不安全构建 Sentry 报告时会被剔除少量字段被库视为PII 安全自动进入报告你还可以把额外字符串选入报告。6.1 自动视为 PII 安全的内容错误对象的类型栈轨迹只含文件路径、行号、函数名不含参数issue 跟踪链接URL 与 detail 字段遥测键错误域上下文标签的键Newf、AssertionFailedf等...f()构造器的格式串...f()构造器附加参数的类型已知 PII 安全的具体参数类型的值细节见redact包。6.2 将额外信息选入上报三种方式实现errors.SafeDetailer接口在自定义错误类型上提供SafeDetails() []string方法用errors.Safe()包裹...f()构造器的参数err : errors.Newf(my code: %d, errors.Safe(123))此时123会进入 Sentry 报告可通过errors.GetSafeDetails()/GetAllSafeDetails()获取同时它仍是Error()主消息的一部分。WithSafeDetails附加任意字符串err errors.WithSafeDetails(err, additional data: %s, errors.Safe(hello))hello会进入 Sentry 报告但不是Error()主消息的一部分。Sentry 报告的构建细节集中在 report 子包中。七、把自定义错误类型接入库编码、解码与格式化自定义错误类型不需要继承任何基类——只需按 Go 惯例实现接口实现error接口若是包装器再实现errors.Wrapper即Unwrap()方法为兼容pkg/errors可额外实现Cause()若是包装器应实现Format()并重定向到errors.FormatError()否则%v失效若类型带有Error()之外的有效载荷可再实现errors.SafeFormatter。7.1 最小解码器只需一个函数库能自动完成大部分编解码工作唯独用你的新类型实例化一个 Go 对象这件事必须由你提供解码器// 注意这里使用 gogoproto 的 proto 子包。 func yourDecode(_ string, _ []string, _ proto.Message) error { return yourType{} } func init() { errors.RegisterLeafEncoder((*yourType)(nil), yourDecodeFunc) } func yourDecodeWrapper(cause error, _ string, _ []string, _ proto.Message) error { // 注意库已经负责对 cause 进行编解码。 return yourWrapperType{cause: cause} } func init() { errors.RegisterWrapperDecoder((*yourWrapperType)(nil), yourDecodeWrapper) }若类型没有其他字段叶子为空结构体、包装器只有 cause做到这一步即可。库内withAssertionFailure类型assert/assert.go就是这一简单情形的范例。7.2 有额外字段时利用消息与 SafeDetails 自动编码库会自动编码Error()的结果以及SafeDetailer暴露的安全字符串并作为参数回传给解码器。例如type myLeaf struct { code int } func (m *myLeaf) Error() string { return fmt.Sprintf(my error: %d, m.code) }解码器可以直接从消息字符串中还原codefunc myLeafDecoder(msg string, _ []string, _ proto.Message) error { codeS : strings.TrimPrefix(msg, my error: ) code, _ : strconv.Atoi(codeS) // 说明此处为简化省略了 strconv 的错误处理。 // 解码失败时应返回 nil 错误对象而非另一个无关错误。 return myLeaf{code: code} }若字段是 PII 安全的最好通过SafeDetails()暴露——这样即使远端系统不认识你的类型也能生成有用的 Sentry 报告func (m *myLeaf) SafeDetails() []string { return []string{fmt.Sprintf(%d, m.code), m.tag} }解码器随后可从details切片还原字段。参考实现见 telemetry/with_telemetry.go 中的withTelemetry类型。唯一需要自定义编码器的情形错误类型包含某些字段既无法从错误消息中还原又因含 PII 而不能作为安全细节上报。需要自定义编码器的库内范例包括提示/详情hintdetail/with_hint.go、hintdetail/with_detail.go、次要错误包装器secondary/with_secondary.go、标记包装器markers/markers.go。7.3 让%v对你的类型生效原则非常简单有疑问时一律实现fmt.Formatter并精确地重定向func (e *yourType) Format(s *fmt.State, verb rune) { errors.FormatError(e, s, verb) }不提供此重定向会禁用%v对包装器 cause 链的递归格式化。可选实现errors.SafeFormatterSafeFormatError(p errors.Printer) (next error)。当某些细节未包含在Error()中、但应在%v时输出就应该实现它。库内withHTTPCode包装器exthttp/ext_http.go是标准范例// Format() 实现 fmt.Formatter在 Go 标准库学会 FormatError 之前是必需的。 func (w *withHTTPCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) } // SafeFormatError() 格式化错误。 func (w *withHTTPCode) SafeFormatError(p errors.Printer) (next error) { // 注意无需在这里打印 cause // FormatError() 会自动处理。 if p.Detail() { p.Printf(http code: %d, errors.Safe(w.code)) } return w.cause }技术背景该库遵循 Go 2 错误值提案go#29934。在未来标准库fmt学会自动识别错误包装器之前你必须手动实现Format()重定向也可以只实现fmt.Formatter而不实现errors.Formattererrors.FormatError会走另一条做正确的事的代码路径。7.4 跨版本重命名RegisterTypeMigration当包含自定义错误类型的 Go 包被重命名、或类型本身被重命名且该错误会跨网络传输时运行不同版本软件的系统可能无法再用errors.Is识别它。解法是在init()中调用errors.RegisterTypeMigration()previousPath : github.com/old/path/to/error/package previousTypeName : oldpackage.oldErrorName newErrorInstance : newTypeName{...} errors.RegisterTypeMigration(previousPath, previousTypeName, newErrorInstance)这与编码端的实现呼应errbase.getTypeDetails在计算类型族名family name时会查询backwardRegistry反向迁移注册表把旧类型键映射回新类型见 errbase/encode.go从而保证新旧版本之间errors.Is仍然成立。八、网络传输原理protobuf 编码与 opaque 兜底网络可移植性是本库区别于其他错误库的根本能力其实现集中在 errbase/encode.go 与 errbase/decode.go编码EncodeError按错误是否为包装器分别走encodeWrapper或encodeLeaf多 cause 错误也编码为 Leaf 形态以保持向后兼容。编码时先查找手动注册的 encoder没有注册时从Error()提取消息、从SafeDetailer提取可上报载荷若类型恰好实现proto.Message则把 payload 用types.Any编码进FullDetails。类型键TypeKey由reflect推导的包路径 类型名构成如github.com/xxx/yyy / *myType作为查找 decoder 与比较错误身份的族名。解码DecodeError先尝试注册的 decoder 与多 cause decoder若 payload 本身是error则直接返回全部失败时降级为opaqueLeaf/opaqueWrapper——保留收到的消息、类型标记与细节既能再次原样编码转发也让Is()得以通过类型标记继续工作。这正是认识不了类型也能处理的前向兼容实现。使用者只需调用enc : errors.EncodeError(ctx, err) // 得到 protobuf 可编码的 EncodedError err2 : errors.DecodeError(ctx, enc) // 还原必要时为 opaque 形态九、非构造类 API 速查原文档还总结了全部非构造类 API按功能分组如下详细文档见 pkg.go.dev 对应页面// 访问原因链。 func UnwrapAll(err error) error func UnwrapOnce(err error) error func Cause(err error) error // 兼容 func Unwrap(err error) error // 兼容 type Wrapper interface { ... } // 兼容 // 错误格式化。 type Formatter interface { ... } // 兼容不推荐 type SafeFormatter interface { ... } type Printer interface { ... } func FormatError(err error, s fmt.State, verb rune) func Formattable(err error) fmt.Formatter // 识别错误。 func Is(err, reference error) bool func IsAny(err error, references ...error) bool func If(err error, pred func(err error) (interface{}, bool)) (interface{}, bool) func As(err error, target interface{}) bool // 编解码错误。 type EncodedError // protobuf 可编码 func EncodeError(ctx context.Context, err error) EncodedError func DecodeError(ctx context.Context, enc EncodedError) error // 为自定义错误类型注册编解码函数。 func RegisterLeafDecoder(typeName TypeKey, decoder LeafDecoder) func RegisterLeafEncoder(typeName TypeKey, encoder LeafEncoder) func RegisterWrapperDecoder(typeName TypeKey, decoder WrapperDecoder) func RegisterWrapperEncoder(typeName TypeKey, encoder WrapperEncoder) func RegisterWrapperEncoderWithMessageOverride(typeName TypeKey, encoder WrapperEncoderWithMessageOverride) func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder) func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) type LeafEncoder func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message) type LeafDecoder func(ctx context.Context, msg string, safeDetails []string, payload proto.Message) error type WrapperEncoder func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message) type WrapperEncoderWithMessageOverride func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool) type WrapperDecoder func(ctx context.Context, cause error, msgPrefix string, safeDetails []string, payload proto.Message) error type MultiCauseEncoder func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message) type MultiCauseDecoder func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error // 注册自定义错误类型的包重命名。 func RegisterTypeMigration(previousPkgPath, previousTypeName string, newType error) // Sentry 报告。 func BuildSentryReport(err error) (*sentry.Event, map[string]interface{}) func ReportError(err error) (string) // 栈轨迹捕获。 func GetOneLineSource(err error) (file string, line int, fn string, ok bool) type ReportableStackTrace sentry.StackTrace func GetReportableStackTrace(err error) *ReportableStackTrace // 安全PII 无关细节。 type SafeDetailPayload struct { ... } func GetAllSafeDetails(err error) []SafeDetailPayload func GetSafeDetails(err error) (payload SafeDetailPayload) // 已废弃 API。 type SafeMessager interface { ... } func Redact(r interface{}) string // redact.Safe 的别名。 func Safe(v interface{}) SafeMessager // 断言失败。 func HasAssertionFailure(err error) bool func IsAssertionFailure(err error) bool // 面向用户的详情与提示。 func GetAllDetails(err error) []string func FlattenDetails(err error) string func GetAllHints(err error) []string func FlattenHints(err error) string // issue 链接 / URL 包装器。 func HasIssueLink(err error) bool func IsIssueLink(err error) bool func GetAllIssueLinks(err error) (issues []IssueLink) // 未实现错误。 func HasUnimplementedError(err error) bool func IsUnimplementedError(err error) bool // 遥测键。 func GetTelemetryKeys(err error) []string // 错误域。 type Domain const NoDomain Domain func GetDomain(err error) Domain func NamedDomain(domainName string) Domain func PackageDomain() Domain func PackageDomainAtDepth(depth int) Domain func EnsureNotInDomain(err error, constructor DomainOverrideFn, forbiddenDomains ...Domain) error func NotInDomain(err error, doms ...Domain) bool // 上下文标签。 func GetContextTags(err error) []*logtags.Buffer几个值得注意的使用要点errors.IsAny()本库独有可一次性比对多个参考错误oserror子包用oserror.IsPermission()、IsTimeout()、IsExist()、IsNotExist()替代os包同名函数使判断能够穿透多层包装对应 oserror/oserror.goerrors.GetSafeDetails()提取无 PII 安全细节GetAllHints()/FlattenHints()提取面向用户的内容BuildSentryReport()/ReportError()生成可发送给 Sentry.io 的事件对象或报告字符串构建逻辑见 report/report.go其中注释详细描述了 Sentry 事件字段与可视化呈现的映射关系。十、错误组合一览Summary原文档给出的构造器组合关系表是理解每个构造器背后发生了什么的捷径构造器组合内容NewNewWithDepth见下ErrorfNewfNewfNewWithDepthf见下WithMessage带消息前缀与安全字符串认知的自定义包装器WrapWrapWithDepth见下WrapfWrapWithDepthf见下AssertionFailedAssertionFailedWithDepthf见下NewWithDepth带安全字符串认知的自定义叶子 WithStackDepth见下NewWithDepthf自定义叶子 WithSafeDetailsWithStackDepthWithMessagef带消息前缀与安全字符串认知的自定义包装器WrapWithDepthWithMessageWithStackDepthWrapWithDepthfWithMessagefWithStackDepthAssertionFailedWithDepthfNewWithDepthfWithAssertionFailureNewAssertionErrorWithWrappedErrfHandledWithMessagef屏障WrapWithDepthfWithAssertionFailureJoinJoinWithDepth见下JoinWithDepth多 cause 包装器 WithStackDepth从AssertionFailedWithDepthf的源码实现errutil/assertions.go可见这种组合是字面意义上的NewWithDepthf(...)之后立即assert.WithAssertionFailure(...)而NewAssertionErrorWithWrappedErrDepthf则是barriers.Handled→WrapWithDepthf→WithAssertionFailure三步链同文件第 73-80 行。十一、在本仓库中的集成形态本仓库将该库以间接依赖的形式引入版本为v1.11.3见 go.mod 第 151 行源码完整 vendored 于vendor/github.com/cockroachdb/errors/目录下。目录结构清晰地映射了文档中的各功能分区顶层 API 门面*_api.go系列文件如 errutil_api.go、markers_api.go、errbase_api.go、report_api.go向用户暴露统一入口子包实现errutil构造器与工具、errbase编解码与格式化核心、markersIs/Mark实现、reportSentry 报告、hintdetail、secondary、withstack、oserror、domains、contexttags 等protobuf 定义errorspb 子包内含errors.proto、hintdetail.proto、markers.proto、tags.proto、testing.proto及其生成的.pb.go即网络传输的线格式定义。若要在自己的 Go 项目中复用它只需按前述使用方式一节导入并调用即可errors.New/Wrap/Is/EncodeError等 API 均为顶层导出无额外初始化步骤自定义类型接入时在包init()中注册编解码函数与类型迁移即可。结语cockroachdb/errors的价值不在于又一个Wrap/Is封装而在于把错误从进程内的值提升为可跨网络传输、可跨版本识别、可安全上报的一等公民通过 protobuf 编码与 opaque 兜底实现前向兼容通过错误标记mark实现跨网络Is识别通过默认脱敏 显式选入的策略解决分布式系统中 PII 泄露的合规痛点。对构建多服务、多版本混跑架构的 Go 团队而言这套设计为错误观测与治理提供了一个相当完整的参考范式——正如其在 CockroachDB 生产环境中所承担的角色。【免费下载链接】inngestThe leading workflow orchestration platform. Run stateful step functions and AI workflows on serverless, servers, or the edge.项目地址: https://gitcode.com/GitHub_Trending/in/inngest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →