216 lines
6.3 KiB
Markdown
216 lines
6.3 KiB
Markdown
# 用 HTTP 状态码还是自定义状态码? Go 错误处理的优雅解决方案
|
||
|
||
在开发 REST API 时,你是否遇到过这样的问题:
|
||
|
||
- 错误信息杂乱无章,难以定位问题
|
||
- 用户看到的错误提示晦涩难懂
|
||
- 生产环境暴露了敏感信息
|
||
- 错误处理代码重复且难以维护
|
||
|
||
本文将介绍一种优雅的错误处理解决方案,让你的代码更加清晰、可维护,同时提供更好的用户体验。
|
||
|
||
## 错误处理的痛点
|
||
|
||
在传统的错误处理方式中,我们常常会遇到以下问题:
|
||
|
||
1. 错误信息不统一
|
||
- 有的使用数字状态码
|
||
- 有的使用字符串描述
|
||
- 有的直接返回底层错误
|
||
|
||
2. 错误处理代码重复
|
||
- 每个接口都要写错误处理逻辑
|
||
- 日志记录分散在各处
|
||
- HTTP 状态码映射混乱
|
||
|
||
3. 用户体验差
|
||
- 错误提示不友好
|
||
- 缺乏上下文信息
|
||
- 难以定位问题
|
||
|
||
## 传统错误处理方案
|
||
|
||
让我们先看看传统的错误处理方式:
|
||
|
||
```go
|
||
// 方式一:直接返回错误
|
||
func findUser(ctx *gin.Context) {
|
||
user, err := db.FindUser()
|
||
if err != nil {
|
||
ctx.JSON(500, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
ctx.JSON(200, user)
|
||
}
|
||
|
||
// 方式二:使用状态码
|
||
func findUser(ctx *gin.Context) {
|
||
user, err := db.FindUser()
|
||
if err != nil {
|
||
ctx.JSON(500, gin.H{
|
||
"code": 1001,
|
||
"msg": "数据库查询失败"
|
||
})
|
||
return
|
||
}
|
||
ctx.JSON(200, user)
|
||
}
|
||
```
|
||
|
||
这些方案虽然简单直接,但存在一些明显的缺陷:
|
||
|
||
1. 安全性问题
|
||
- 方式一直接将底层错误暴露给用户,可能泄露敏感信息
|
||
- 数据库错误可能包含表结构、SQL 语句等内部信息
|
||
|
||
2. 沟通复杂性
|
||
- 错误状态码 400 是 http 状态码还是自定义状态码?
|
||
- 必须对响应体反序列化,才能知道状态
|
||
- 重复定义,http 状态码 200 与自定义状态码 0 都表示成功
|
||
|
||
3. 可维护性差
|
||
- 数字错误码(如 1001)缺乏语义,难以理解
|
||
- 开发者需要查阅文档才能理解错误含义
|
||
- 错误码定义者也可能忘记具体含义
|
||
|
||
## 优雅的错误处理方案
|
||
|
||
让我们先看看优雅的错误处理方案是如何使用的:
|
||
|
||
```go
|
||
var ErrBadRequest = reason.NewError("ErrBadRequest", "请求参数有误")
|
||
|
||
func (u *UserAPI) getUser(ctx *gin.Context, _ *struct{}) (*user.UserOutput, error) {
|
||
return u.core.GetUser(in.ID)
|
||
}
|
||
|
||
// package user
|
||
func (u *Core) GetUser(id int64) (*UserOutput, error) {
|
||
// 参数校验
|
||
if err != nil {
|
||
return nil, ErrBadRequest.With(err.Error(), "xx 参数应在 10~100 之间")
|
||
}
|
||
// 正确处理逻辑...
|
||
}
|
||
```
|
||
|
||
还记得上一篇文章提到的 `web.WrapH` 函数吗? 其响应错误实际是调用的 `web.Fail(err)`,此方法会判断错误是否是 `reason.Error` 类型,如果是,则按照其定义的 http 状态码,reason, msg 等信息返回给客户端。
|
||
|
||
类似
|
||
|
||
HTTP Status Code: 400 (默认所有错误都是 400)
|
||
```json
|
||
{
|
||
"reason": "用于程序识别的错误",
|
||
"msg": "告诉用户的错误信息描述",
|
||
"details":[
|
||
"某字段传输有误",
|
||
"你可以这样修复",
|
||
"查看文档获取更多信息"
|
||
]
|
||
}
|
||
```
|
||
|
||
### 错误码设计思考
|
||
|
||
传统方案中使用数字错误码(如1001表示数据库错误)存在明显缺点:缺乏语义性,需查阅文档理解,定义者也易忘记含义。
|
||
|
||
因此,我们采用字符串作为错误码,优势明显:
|
||
|
||
1. 自解释性强
|
||
- `ErrBadRequest`比`1001`直观明了
|
||
- 错误码即文档
|
||
- 便于代码审查和调试
|
||
|
||
2. 扩展性好
|
||
- 可用模块前缀区分
|
||
- 避免错误码冲突
|
||
- 快速定位问题源
|
||
|
||
在和前端的对接过程中,某些接口出现错误了,前端会一头雾水,找服务端排查,有些可能只是参数问题,如果在响应的错误中有帮助解决的方案呢?能否简化对接复杂度?
|
||
|
||
为避免用户看到技术性错误,或者开发者缺乏上下文信息。我们将错误信息分为四个属性:
|
||
|
||
```go
|
||
type Error struct {
|
||
Reason string
|
||
Msg string
|
||
Details []string
|
||
HTTPStatus int
|
||
}
|
||
```
|
||
|
||
每个字段的作用:
|
||
|
||
1. `reason` 字段
|
||
- 使用大驼峰英语描述错误原因
|
||
- 用于程序内部判断错误类型
|
||
- 支持错误码映射到 HTTP 状态码
|
||
|
||
2. `msg` 字段
|
||
- 使用开发者母语描述错误
|
||
- 面向用户,提供友好的提示
|
||
|
||
3. `details` 字段
|
||
- 提供错误扩展信息
|
||
- 面向开发者,帮助调试
|
||
- 可在生产环境调用 `web.SetRelease()` 隐藏,避免泄露敏感信息
|
||
|
||
4. `HTTPStatus`
|
||
- http 响应状态码
|
||
- 默认为 400
|
||
- 常用状态码 200,400,401 基本就够了,保持简单,少即是多
|
||
|
||
## 使用文档
|
||
|
||
1. 使用预定义的错误类型
|
||
- `reason.ErrBadRequest`: 请求参数错误
|
||
- `reason.ErrStore`: 数据库错误
|
||
- `reason.ErrServer`: 服务器错误
|
||
|
||
2. 错误信息处理
|
||
- 使用 `SetMsg()` 方法修改用户友好提示
|
||
- 使用 `Withf()` 方法添加开发者帮助,增加错误上下文
|
||
|
||
采用一个 reason 原则,即 reason 相同则是同一个错误。
|
||
|
||
```go
|
||
// e1 和 e2 是不是同一个错误!
|
||
e1 = NewError("e1", "e1")
|
||
e2 = NewError("e2", "e1")
|
||
```
|
||
|
||
```go
|
||
// e3 与 e2 是相同的错误
|
||
e2 := NewError("e2", "e2").SetHTTPStatus(200).With("e2-1")
|
||
e3 := fmt.Errorf("e3:%w", e2)
|
||
if !errors.Is(e3, e2) {
|
||
t.Fatal("expect e3 is e2, but not")
|
||
}
|
||
```
|
||
|
||
```go
|
||
// 将错误转换为 *reason.Error 结构体
|
||
var e5 *reason.Error
|
||
if !errors.As(e4, &e5) {
|
||
t.Fatal("expect e4 as e5, but not")
|
||
}
|
||
```
|
||
|
||
## 总结
|
||
|
||
通过合理的分层和封装,我们实现了:
|
||
|
||
1. 统一的错误处理流程
|
||
2. 友好的用户提示
|
||
3. 详细的开发者信息
|
||
4. 安全的错误暴露
|
||
|
||
这种设计既保证了开发效率,又提升了用户体验。如果你正在寻找一个优雅的错误处理解决方案,不妨试试这个方案。
|
||
|
||
## 关于 goddd
|
||
|
||
本文介绍的错误处理是 [goddd](https://github.com/ixugo/goddd) 项目中的一个核心组件。goddd 是一个基于 DDD(领域驱动设计)理念的 Go 项目目标,它提供了一系列工具和最佳实践,帮助开发者构建可维护、可扩展的应用程序。
|
||
|
||
如果你对本文介绍的内容感兴趣,欢迎访问 [goddd 项目](https://github.com/ixugo/goddd) 了解更多细节。项目提供了完整的示例代码和详细的文档,可以帮助你快速上手。
|