errors
模块设计:errors
为什么要再设计errors
为什么要提供一个errors模块呢?当然是为了更方便地错误处理。go标准库提供了package errors用来定义一些错误,这些错误可以用于在函数调用过程中传递、返回、执行比较判断等等,而且自go1.13开始吧也支持了errors.Wrap/Unwrap/Is等操作允许对error进行包装/解开/判断等能力,对于常规错误处理而言,是绰绰有余了。
但是对于一个服务而言,其内部可观测到的错误,除了业务逻辑错误,还有底层框架观测到的错误。希望有能力能对这两种类型的错误进行区分,以方便框架、业务方做相应的处理。
比如以转账为例,一次转账RPC调用,网络超时是框架错误,而转出余额不足则是逻辑错误。针对两种类型的错误可以执行不同的错误处理逻辑,网络超时可以采用重试,余额不足则可以短信通知用户失败。
允许识别错误的类型
如何区分是哪种类型的错误呢?
- 假如沿用go的使用习惯,可能会先定义一个
var errFramework = errors.New("framework error")
,凡是框架类错误都通过fmt.Errorf("%w: %s", errFramework, err)
来声明,后续判断是否是框架错误的时候可以通过errors.Is(err, errFramework)
来判断。通过这种方式来区分框架、业务错误已经足够了,但是继续考虑下RPC框架中涉及到C/S之间的通信,当server返回一个框架错误信息给client,序列化的时候没有把当前err包含的errFramework包含进去或者即便包含了client端也不不方便识别,这种实现就又变得是问题。 - 另一种方式就是定义一个Error类型,其中包含错误类型(框架错误或服务逻辑错误)、错误码、错误描述,设计如下图所示。Error实现了error接口,其Error()方法用于展示错误码、描述、类型的格式化字符串。Error的类型主要包含框架错误、业务逻辑错误。这种方式在RPC通信时也比较好处理,直接把Error中的部分或者全部字段纳入序列化流程,序列化完成后、编码成响应包、发送即可,就解决了上面这点提到的问题。
errors模块的设计实现
综合上述方式考虑,我们建议采用方法二,在此基础上进行模块化设计,设计如下所示:
设计实现errors的时候,还应该考虑到这里的框架errors是否需要导出,哪些应该导出,各个错误对应的错误码、错误描述、类型要统一整理好。
框架内统一维护errors
为了方便业务开发者查询、比较error,最好能将error统一整理,框架、业务代码中都建议这样做:
var (
errXXX = newError(1000, ".........", ErrorTypeFramework)
errYYY = newError(1001, ".........", ErrorTypBusiness
ErrZZZ = NewError(1002, ".........")
)
合理封装隐藏实现细节
作为框架使用者肯定是希望越简单越好,而这里的简单也要考虑使用时的学习成本、记忆成本,如果本不需要使用者关心的信息却暴露给了使用者,进而增加了很多记忆成本,这无疑是一种心理负担。
带着这种思考我们来考虑几个问题:
业务开发者要关心框架的错误码范围吗?
在协议设计过程中一般为了指示错误,需要在协议头中包括错误码字段。怎么理解错误码的具体含义呢?要么通信双方都有一个错误码对应描述的本本,但是由于错误码是可扩展的,通信双方不一定都能及时更新,那就对不上了。所以协议头里面增加一个额外的错误码描述是常用手段。
以HTTP/1.1为例,当我们发送一个非法的HTTP请求时,会返回“HTTP/1.1 400 Bad Request
”,其中400是错误码,Bad Request是错误描述。在其他应用层协议设计中也基本是采用错误码+错误描述的设计。
框架有框架的错误码,业务有自己定义的逻辑错误码,如果二者使用的错误码范围重叠,那么可能会导致框架的某些错误处理逻辑判断失败,误将某些普通逻辑错误当成框架中的错误,等等。如果框架是依靠错误码范围来区分错误类型,那么就需要业务开发者记忆这里的错误码范围,这无疑是增加了使用负担。
而如果在错误中引入错误类型的概念,则可以较好地解决这个问题,至少业务开发者不用再去记忆框架、业务各自应使用的错误码范围。
业务开发者要关心框架支持的错误类型吗?
业务开发者在创建Error的时候,创建的只可能是业务逻辑错误,而不可能是框架错误。业务开发者在执行一个RPC调用的时候,如果发生了错误需要区分错误类型,并根据是框架错误、逻辑错误做进一步的错误处理。
因此,框架暴露给业务开发者的函数,在创建Error时可以完全忽略掉错误类型,而如果框架内部需要这样的支持错误类型的导出函数,则可以通过“internal
”包管理机制将相应的导出函数仅对当前go module可见。同时提供便利的包函数来判断error类型、提取错误码和描述信息。
总结
我们简单介绍了设计errors模块的初衷和设计理念,errors模块在框架代码、业务代码中会被广泛使用,在后续模块的设计实现中,我们会再介绍errors的使用。