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