go zero 错误处理(统一响应信息)
一、为什么需要统一错误处理机制
1.1 传统HTTP状态码的局限性
在典型的RESTful API设计中,我们通常会使用HTTP状态码来表示请求结果。但在实际业务场景中,这种设计存在明显不足:
-
业务错误表达不充分:如用户重复注册(错误码409)、余额不足(业务错误)等场景,HTTP状态码无法精确描述
-
前端处理复杂度高:需要同时处理HTTP状态码和业务错误码两套体系
-
监控统计困难:HTTP 400系列错误可能包含多种业务场景,难以精确统计
例如,在上一篇的文章中,我们尝试重复注册的时候,给我们返回来400状态码,这样不利于前端来做用户提示。
1.2 统一响应格式
在API服务中,我们希望HTTP接口返回的状态码永远是200,通过业务自定义的错误码来区分业务错误或者服务内部错误。这样做可以让前端开发者只需关注业务状态码,无需额外处理HTTP状态码。
<pre>// 成功响应
{
"code": 0,
"msg": "success",
"data": {...}
}
// 错误响应
{
"code": 1001,
"msg": "用户已存在"
}</pre>
当返回错误的时候,HTTP接口同样也返回JSON格式的数据,code为业务自定义的错误码,message为自定义错误码的详细描述,客户端获取到该数据后可以判断该业务错误码,同时可以把message信息做一个错误弹窗处理。
这样做的优势有:
- 前端统一处理逻辑:前端只需关注业务状态码,简化错误处理流程
- 更细粒度的错误分类:可以针对不同业务场景定义明确的错误码
- 更好的可扩展性:随着业务发展可以不断扩充错误定义
- 降低跨团队沟通成本:统一的错误码规范便于前后端协作
- 便于错误追踪:可以更容易识别和分析系统中的错误
- 支持国际化:错误信息可以基于错误码实现多语言支持
1.3 错误码设计原则
设计良好的错误码系统应遵循以下原则:
- 分层设计:按模块或功能领域进行分组,如用户模块错误码以1xxx开头
- 语义明确:错误码和错误信息应明确描述问题
- 保持稳定:一旦定义发布,尽量不要变更已有错误码含义
二、自定义错误处理方法和自定义错误码
自定义错误处理方法,需要使用httpx.SetErrorHandler()
,来捕捉错误信息。
2.1 错误结构设计
我们在项目所在目录下新建biz
目录,然后再这个目录下分别创建3个文件
err.go 用来定义错误结构:
package biz
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e *Error) Error() string {
return e.Msg
}
func NewError(code int, msg string) *Error {
return &Error{
Code: code,
Msg: msg,
}
}
2.2 统一响应格式
resp.go 用来处理错误响应
package biz
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
func Success(data any) *Result {
return &Result{
Code: Ok,
Msg: "success",
Data: data,
}
}
func Fail(err *Error) *Result {
return &Result{
Code: err.Code,
Msg: err.Msg,
}
}
func ErrHandler(err error) (int, any) {
switch e := err.(type) {
case *Error:
// 自定义一个 错误返回类型
return http.StatusOK, Fail(e)
default:
return http.StatusInternalServerError, nil
}
}
2.3 定义业务通用错误码
vars.go 用来定义比较通用的业务错误码
package biz
// 通用状态码
const (
Ok = 0 // 成功
SystemError = 500 // 系统错误
ParamError = 400 // 参数错误
Unauthorized = 401 // 未授权
Forbidden = 403 // 禁止访问
NotFound = 404 // 资源不存在
ServiceUnavailable = 503 // 服务不可用
)
// 用户模块错误码 (1000-1999)
var (
AlreadyRegister = NewError(1001, "用户已注册")
PasswordErr = NewError(1002, "密码错误")
UserNotExist = NewError(1003, "用户不存在")
InsertErr = NewError(1004, "用户注册失败")
TokenInvalid = NewError(1005, "身份验证失败")
AccountLocked = NewError(1006, "账号已被锁定")
)
// 订单模块错误码 (2000-2999)
var (
OrderNotExist = NewError(2001, "订单不存在")
PaymentFailed = NewError(2002, "支付失败")
StockShortage = NewError(2003, "库存不足")
OrderCancelled = NewError(2004, "订单已取消")
)
// 支付模块错误码 (3000-3999)
var (
BalanceInsufficient = NewError(3001, "余额不足")
PaymentTimeout = NewError(3002, "支付超时")
RefundFailed = NewError(3003, "退款失败")
)
// 可以继续添加其他模块的错误码...
2.4 注册全局错误处理器
接着修改user.go
文件,在main函数中使用自定义错误处理方法:
/*
....
*/
defer server.Stop()
//httpx.SetErrorHandler 函数可以帮助你定义一个全局的错误处理逻辑,
//该逻辑会在 HTTP handler 中捕获到的所有错误中执行。
//它将允许你统一处理各类错误,返回更加一致和用户友好的响应。
//httpx.SetErrorHandler 仅在调用了 httpx.Error 处理响应时才有效。
httpx.SetErrorHandler(biz.ErrHandler)
ctx := svc.NewServiceContext(c)
/*
....
*/
接着修改业务代码,我们还是以注册功能为例,把返回的错误信息修改成我们自定义错误:
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.RegisterResponse, err error) {
// todo: add your logic here and delete this line
/*
...
*/
if user != nil {
//return nil, errors.New(1, "用户已注册")
return nil, biz.AlreadyRegister
}
//插入新的数据
/*
...
*/
if err != nil {
//return nil, errors.New(2, "用户注册失败")
return nil, biz.InsertErr
}
}
接着运行测试
三、 使用zeromicro/x库实现
如果你不想自己编写错误处理方法,也可以使用go zero 官方提供的X仓库,可以实现统一响应格式
3.1 下载库
go get github.com/zeromicro/x
它会自动帮我们把响应信息改为下面这种格式:
{
"code": 0,
"msg": "ok",
"data": {
...
}
}
3.2 修改handler
使用这个库需要我们修改handler
,把原来的错误处理替换掉:
//导入zeromicro库并设置别名,避免和原生的http冲突
import (
xhttp "github.com/zeromicro/x/http"
)
//修改RegisterHandler的返回信息
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterRequest
if err := httpx.Parse(r, &req); err != nil {
//使用xhttp.JsonBaseResponseCtx 替换掉httpx.ErrorCtx
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
//httpx.ErrorCtx(r.Context(), w, err)
return
}
l := register.NewRegisterLogic(r.Context(), svcCtx)
resp, err := l.Register(&req)
if err != nil {
//使用xhttp.JsonBaseResponseCtx 替换掉httpx.ErrorCtx
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
//httpx.ErrorCtx(r.Context(), w, err)
} else {
//使用xhttp.JsonBaseResponseCtx 替换掉httpx.OkJsonCtx
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
//httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}
3.3 修改返回错误
在业务文件中,把原来的err
修改成 errors.New()
,它的参数有两个,一个是用来返回 code码 ,还有一个是message消息:
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.RegisterResponse, err error) {
// todo: add your logic here and delete this line
/*
.....
*/
if user != nil {
//return nil, err
return nil, errors.New(1, "用户已注册")
}
//插入新的数据
/*
.....
*/
if err != nil {
//return nil, err
return nil, errors.New(2, "用户注册失败")
}
}
接着我们运行项目,使用Postman重新测试,结果如下: