关于错误返回,只要是非业务错误,http状态码一律返回200。目的就是为了让前端统一处理错误,减少因为 HTTP 状态码不规范导致的扯皮。
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
........
if err != nil {
// 查无用户,业务错误
if err == sql.ErrNoRows {
return &types.LoginResp{
Base: types.Base{
Code: consts.ErrUserNotRegister,
Msg: consts.GetRespMsg(consts.ErrUserNotRegister),
},
}, nil
}
// 数据库真实故障:连接、超时、SQL错误等
logx.Error("查询用户失败", logx.Field("email", phone), logx.Field("err", err))
return &types.LoginResp{
Base: types.Base{
Code: consts.ErrDBFail,
Msg: consts.GetRespMsg(consts.ErrDBFail),
},
}, nil
}
这样些虽然很合理,但是有点啰嗦。因为每次err != nil,都要些return &types.LoginResp{…} 。
而且,在 Go 中,nil 就是“空”,代表“没有错误”。框架(go-zero)收到响应后检查:err == nil , 判定业务执行成功。于是框架直接把您的 LoginResp 结构体序列化成 JSON 扔给前端。异常场景却使用正常返回承载错误。
这时,可以用到go-zero自定义错误处理,把错误码升级为 error 包装。
注册自定义处理器和自定义错误类型后(见第二节),只要写一句,如下:
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
........
// 查无用户,业务错误
if err == sql.ErrNoRows {
return nil, errors.NewBusinessError(consts.ErrDBFail)
}
手动将自定义错误对象塞进了 error ,且这个对象不是 nil。框架检查发现 err != nil ,判定业务执行失败。
于是框架不再返回 LoginResp,而是将 err 丢给 httpx.SetErrorHandler (即自定义错误处理器)去处理。
前端返回效果如下:

http状态码,还是200,返回里包装业务错误码。
二:自定义错误处理器和自定义业务错误
// 业务错误定义
package errors
import (
"fmt"
"gitee.com/zhencz/zhujian/internal/consts"
)
type BusinessError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e *BusinessError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Msg)
}
func NewBusinessError(code int) *BusinessError {
return &BusinessError{
Code: code,
Msg: consts.GetRespMsg(code),
}
}
mian函数中注册,必须在 server.Start () 之前注册。
func main() {
....
httpx.SetErrorHandler(func(err error) (int, any) {
if bizErr, ok := err.(*errors.BusinessError); ok {
// 业务错误 HTTP 200
return http.StatusOK, map[string]any{
"code": bizErr.Code,
"msg": bizErr.Msg,
}
}
// 系统错误 HTTP 500
return http.StatusInternalServerError, map[string]any{
"code": 500,
"msg": err.Error(),
}
})
}
业务函数调用执行:
return nil, errors.NewBusinessError(consts.ErrDBFail)
logic 返回 nil, err,err != nil,go-zero rest 框架进入错误处理分支,调用我们注册的 errorHandler,类型断言区分:
*BusinessError:业务异常,HTTP 200 + 返回 {code,msg}
其他 error:系统异常,默认错误处理器
注意:一定要注册SetErrorHandler。如果main.go 中没有写那段判断代码时,go-zero 就会使用它的默认错误处理器。
Logic 返回:return nil, errors.NewBusinessError(consts.ErrUserNotRegister),框架捕获到 err != nil,执行默认错误逻辑,前端返回截图如下。

http状态码不是200了,前端会直接走进 axios 的 catch(错误拦截) ,业务错误和系统错误在前端看来完全没有区别(全都是 HTTP 400)。精心设计的 errCode(如 2001、2002)变成了纯字符串,如上图。前端根本无法通过 res.code 做逻辑判断,“防扯皮”策略直接宣告失败。
SetErrorHandler实现原理
源码位置:go-zero@v1.10.2\rest\httpx\responses.go
func SetErrorHandler(handler func(error) (int, any)) {
errorLock.Lock()
defer errorLock.Unlock()
errorHandler = func(_ context.Context, err error) (int, any) {
return handler(err)
}
}
其中handler ,就是我之前的自定义错误处理器函数。被赋值给了handler 函数变量。
func Error(w http.ResponseWriter, err error, fns ...func(w http.ResponseWriter, err error)) {
doHandleError(w, err, buildErrorHandler(context.Background()), WriteJson, fns...)
}
调用 httpx.Error (…),执行 buildErrorHandler(context.Background()):此时handlerCtx不为nil
func buildErrorHandler(ctx context.Context) func(error) (int, any) {
errorLock.RLock()
handlerCtx := errorHandler // ≠ nil,拿到你设置好的闭包
errorLock.RUnlock()
var handler func(error) (int, any)
if handlerCtx != nil {
handler = func(err error) (int, any) {
return handlerCtx(ctx, err)
}
}
return handler // 返回非nil函数
}
进入 doHandleError,
func doHandleError(...) {
if handler == nil {
// 【不会进这里】默认文本返回分支
}
// 执行你定义的错误处理逻辑
code, body := handler(err)
body不是error类型,调用 writeJson → 输出JSON结构
}