go-zero自定义错误处理器及实现原理

关于错误返回,只要是非业务错误,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结构
}