04 基础 go 包开发 - 错误包
1. 错误返回方法
在 Go 项目开发中,错误的返回方式通常有以下两种:
- 始终返回 HTTP 200 状态码,并在 HTTP 返回体中返回错误信息。
- 返回 HTTP 400 状态码(Bad Request),并在 HTTP 返回体中返回错误信息。
方式一:成功返回,返回体中返回错误信息
例如 Facebook API 的错误返回设计,始终返回 200 HTTP 状态码:
1 | |
在上述错误返回的实现方式中,HTTP 状态码始终固定返回 200,仅需关注业务错误码,整体实现较为简单。然而,此方式存在一个明显的缺点:对于每一次 HTTP 请求,既需要检查 HTTP 状态码以判断请求是否成功,还需要解析响应体以获取业务错误码,从而判断业务逻辑是否成功。理想情况下,我们期望客户端对成功的 HTTP 请求能够直接将响应体解析为需要的 Go 结构体,并进行后续的业务逻辑处理,而不用再判断请求是否成功。
方式二:失败返回,返回体中返回错误信息
Twitter API 的错误返回设计会根据错误类型返回对应的 HTTP 状态码,并在返回体中返回错误信息和自定义业务错误码。成功的业务请求则返回 200 HTTP 状态码。例如:
1 | |
方式二相比方式一,对于成功的请求不需要再次判错。然而,方式二还可以进一步优化:整数格式的业务错误码 215 可读性较差,用户无法从 215 直接获取任何有意义的信息。建议将其替换为语义化的字符串,例如:NotFound.PostNotFound。
Twitter API 返回的错误是一个数组,在实际开发获取错误时,需要先判断数组是否为空,如不为空,再从数组中获取错误,开发复杂度较高。建议采用更简单的错误返回格式:
1 | |
需要特别注意的是,message 字段会直接展示给外部用户,因此必须确保其内容不包含敏感信息,例如数据库的 id 字段、内部组件的 IP 地址、用户名等信息。返回的错误信息中,还可以根据需要返回更多字段,例如:错误指引文档 URL 等。
2. miniblog 错误返回设计与实现
2.1 错误返回方式
miniblog 项目错误返回格式采用了方式二,在接口失败时返回对应的 HTTP/gRPC 状态码,并在返回体中返回具体的错误信息,例如:
1 | |
在错误返回方式二中,需要返回一个业务错误码。返回业务错误码可以带来以下好处:
- **快速定位问题:**开发人员可以借助错误码迅速定位问题,并精确到具体的代码行。例如,错误码可以直接指示问题的含义,同时通过工具(如 grep)轻松定位到错误码在代码中的具体位置;
- **便于排查问题:**用户能够通过错误码判断接口失败的原因,并将错误码提供给开发人员,以便快速定位问题并进行排查;
- **承载丰富信息:**错误码通常包含了详细的信息,例如错误的级别、所属错误类别以及具体的错误描述。这些错误信息可以帮助用户和开发者快速定位问题;
- **灵活定义:**错误码由开发者根据需要灵活定义,不依赖和受限于第三方框架,例如 net/http 和 google.golang.org/grpc;
- **便于逻辑判断:**在业务开发中,判断错误类别以执行对应的逻辑处理是一个常见需求。通过自定义错误码,可以轻松实现。例如:
1 | |
2.2 错误码规范
错误码是直接暴露给用户的,因此需要设计一个易读、易懂且规范化的错误码。在设计错误码时可以根据实际需求自行设计,也可以参考其他优秀的设计方案。
腾讯云 API 3.0 的错误码设计规范:https://github.com/mmungdong/miniblog/blob/master/docs/devel/zh-CN/conversions/error_code.md
腾讯云采用了两级错误码设计。以下是两级错误码设计相较于简单错误码(如 215、InvalidParameter)的优势:
- **语义化:**语义化的错误码可以通过名字直接反映错误的类型,便于快速理解错误;
- **更加灵活:**二级错误码的格式为<平台级.资源级>。其中,平台级错误码是固定值,用于指代某一类错误,客户端可以利用该错误码进行通用错误处理。资源级错误码则用于更精确的错误定位。此外,服务端既可根据需求自定义错误码,也可使用默认错误码。
miniblog 项目预定义了一些平台级错误码:https://github.com/mmungdong/miniblog/blob/master/internal/pkg/errno/code.go
| 错误码 | 错误描述 | 错误类型 |
| OK | 请求成功 | - |
| InternalError | 内部错误 | 1 |
| NotFound | 资源不存在 | 0 |
| BindError | 绑定失败,解析请求体失败 | 0 |
| InvalidArgument | 参数错误(包括参数类型、格式、值等错误) | 0 |
| Unauthenticated | 认证失败 | 0 |
| PermissionDenied | 授权失败 | 0 |
| OperationFailed | 操作失败 | 2 |
表中,错误类型 0 代表客户端错误,1 代表服务端错误,2 代表客户端错误/服务端错误,- 代表请求成功。
2.3 错误包设计
开发一个错误包,需要先为错误包起一个易读、易理解的包名。在 Go 项目开发中,如果自定义包的名称如 errors、context 等,会与 Go 标准库中已存在的 errors 或 context 包发生命名冲突,如果代码中需要同时使用自定义包与标准库包时,通常会通过为标准库包起别名的方式解决。例如,可以通过 import stderrors “errors” 来为标准库的 errors 包定义别名。
为了避免频繁使用这种起别名的操作,在开发自定义包时,可以从包命名上避免与标准库包名冲突。建议将可能冲突的包命名为 <冲突包原始名>x**,**其名称中的“x”代表扩展(extended)或实验(experimental)。这种命名方式是一种扩展命名约定,通常用于表示此包是对标准库中已有包功能的扩展或补充。需要注意的是,这并非 Go 语言的官方规范,而是开发者为了防止命名冲突、增强语义所采用的命名方式。miniblog 项目的自定义 contextx 包也采用了这种命名风格。
因此,为了避免与标准库的 errors 包命名冲突,miniblog 项目的错误包命名为 errorsx,寓意为“扩展的错误处理包”。
miniblog 的错误包为 errno,引用 “github.com/onexstack/onexstack/pkg/errorsx“
由于 miniblog 项目的错误包命名为 errorsx,为保持命名一致性,定义了一个名为 ErrorX 的结构体,用于描述错误信息,具体定义如下:
1 | |
ErrorX 是一个错误类型,因此需要实现 Error 方法:
1 | |
Error() 返回的错误信息中,包含了 HTTP 状态码、错误发生的原因、错误信息和额外的错误元信息。通过这些详尽的错误信息返回,帮助开发者快速定位错误。
提示
miniblog 项目属于 OneX 技术体系中的一个实战项目,其设计和实现方式跟 OneX 技术体系中的其他项目保持一致。考虑到包的复用性,errorsx 包的实现位于 onexstack 项目根目录下的 pkg/errorsx 目录中。
在 Go 项目开发中,发生错误的原因有很多,大多数情况下,开发者希望将真实的错误信息返回给用户。因此,还需要提供一个方法用来设置 ErrorX 结构体中的 Message 字段。同样的,还需要提供设置 Metadata 字段的方法。为了满足上述诉求,给 ErrorX 增加 WithMessage、WithMetadata、KV 三个方法。
1 | |
设置 Message、Metadata 字段的方法名分别为 WithMessage、WithMetadata。WithXXX,在 Go 项目开发中是一种很常见的命名方式,寓意是:设置 XXX。KV 方法则以追加的方式给 Metadata 增加键值对。WithMessage、WithMetadata、KV 都返回了 *ErrorX 类型的实例,目的是为了实现链式调用,例如:
1 | |
在 Go 项目开发中,通常需要将一个 error 类型的错误 err,解析为 *ErrorX 类型,并获取 *ErrorX 中的 Code 字段和 Reason 字段的值。Code 字段可用来设置 HTTP 状态码,Reason 字段可用来判断错误类型, 完整代码参见:https://github.com/onexstack/onexstack/blob/master/pkg/errorsx/errorsx.go。
1 | |
在 Go 项目开发中,经常还要对比一个 error 类型的错误 err 是否是某个预定义错误,因此 *ErrorX 也需要实现一个 Is 方法,Is 方法实现如下:
1 | |
Is 方法中,通过对比 Code 和 Reason 字段,来判断 target 错误是否是指定的预定义错误。注意,Is 方法中,没有对比 Message 字段的值,这是因为 Message 字段的值通常是动态的,而错误类型的定义不依赖于 Message。
至此,成功为 miniblog 开发了一个满足项目需求的错误包 errorsx,代码完整实现见 onexstack 项目的 pkg/errorsx/errorsx.go 文件。
2.4 错误码定义
在实现了 errorsx 错误包之后,便可以根据需要预定义项目需要的错误。这些错误,可以在代码中便捷的引用。通过直接引用预定义错误,不仅可以提高开发效率,还可以保持整个项目的错误返回是一致的。
完整错误码定义参见:https://github.com/onexstack/onexstack/blob/master/pkg/errorsx/code.go
2.5 错误码返回规范
为了标准化接口错误返回,提高接口错误返回的易读性,miniblog 制定了以下错误返回规范:
- 所有接口都要返回 errorsx.ErrorX 类型的错误;
- 建议在错误的原始位置,使用 errno.ErrXXX 方式返回 miniblog 自定义错误类型,其他位置直接透传自定义错误:
1 | |
2.6 错误包测试
Github 地址: https://github.com/onexstack/onexstack/blob/master/pkg/errorsx/errorsx_test.go
1 | |
🤗 总结归纳
- 不同风格的错误返回方式
- miniblog 错误包设计
- 通过定义错误码,使用自定义的错误码规范,再参考一下 miniblog 的错误包示例,可以快速的搭建起项目的错误包
📎 参考文章
- https://articles.zsxq.com/id_7baeuqr15wmo.html
- https://github.com/onexstack/onexstack
- onexstack/miniblog: 微博客:小而美的高质量 Go 实战项目
有关Notion安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~