Skip to content
Go
错误处理与统一响应

错误处理与统一响应

构建统一的错误码体系是 go-zero 工程化的核心环节。本章基于社区标杆项目(如 go-zero-looklook)的实战经验,阐述从 RPC 到 API 的全链路错误处理方案。


核心理念

“一份错误,两处使用”

  • 前端:收到友好、可读的错误提示
  • 日志:记录详细的调试信息(参数、堆栈、trace_id)

错误码体系设计

分层编码规范

编码范围 业务模块 示例
100001 ~ 199999 全局通用错误 参数错误、服务器内部错误
200001 ~ 299999 用户模块 用户不存在、登录失败
300001 ~ 399999 订单模块 订单创建失败、库存不足
400001 ~ 499999 支付模块 支付失败、退款异常
500001 ~ 599999 系统底层错误 DB 异常、Redis 超时、网络错误

💡 uint32 类型可直接转为 gRPC 的 codes.Code,实现错误码跨服务透传。

错误码定义(common/xerr/errcode.go

package xerr

// 成功
const OK uint32 = 0

// ===== 全局通用错误 (100001+) =====
const (
    SERVER_COMMON_ERROR  uint32 = 100001
    REUQES_PARAM_ERROR   uint32 = 100002
    TOKEN_EXPIRE_ERROR   uint32 = 100003
    TOKEN_GENERATE_ERROR uint32 = 100004
    DB_ERROR             uint32 = 100005
    REDIS_ERROR          uint32 = 100006
)

// ===== 用户模块 (200001+) =====
const (
    USER_NOT_FOUND      uint32 = 200001
    USER_ALREADY_EXISTS uint32 = 200002
    WRONG_PASSWORD      uint32 = 200003
)

// ===== 订单模块 (300001+) =====
const (
    ORDER_NOT_FOUND    uint32 = 300001
    ORDER_CANCEL_FAIL  uint32 = 300002
    STOCK_NOT_ENOUGH   uint32 = 300003
)

错误信息映射(common/xerr/errMsg.go

package xerr

var message map[uint32]string

func init() {
    message = make(map[uint32]string)
    message[OK]                   = "成功"
    message[SERVER_COMMON_ERROR]  = "服务器开小差啦,稍后再来试一试"
    message[REUQES_PARAM_ERROR]   = "参数错误"
    message[TOKEN_EXPIRE_ERROR]   = "Token 失效,请重新登录"
    message[DB_ERROR]             = "数据库繁忙,请稍后再试"
    message[USER_NOT_FOUND]       = "用户不存在"
    message[WRONG_PASSWORD]       = "用户名或密码错误"
    message[ORDER_NOT_FOUND]      = "订单不存在"
    message[STOCK_NOT_ENOUGH]     = "库存不足"
}

func MapErrMsg(errcode uint32) string {
    if msg, ok := message[errcode]; ok {
        return msg
    }
    return "服务器开小差啦,稍后再来试一试"
}

func IsCodeErr(errcode uint32) bool {
    _, ok := message[errcode]
    return ok
}

自定义错误类型

// common/xerr/errors.go
package xerr

import "fmt"

// CodeError — 携带业务错误码的错误
type CodeError struct {
    errCode uint32
    errMsg  string
}

func (e *CodeError) GetErrCode() uint32 { return e.errCode }
func (e *CodeError) GetErrMsg() string  { return e.errMsg }
func (e *CodeError) Error() string {
    return fmt.Sprintf("ErrCode: %d, ErrMsg: %s", e.errCode, e.errMsg)
}

// 通过错误码创建
func NewErrCode(errCode uint32) *CodeError {
    return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)}
}

// 自定义消息
func NewErrMsg(errMsg string) *CodeError {
    return &CodeError{errCode: SERVER_COMMON_ERROR, errMsg: errMsg}
}

// 完整指定错误码 + 消息
func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError {
    return &CodeError{errCode: errCode, errMsg: errMsg}
}

RPC 层错误处理

业务逻辑中使用

func (l *GetUserLogic) GetUser(in *rpc.GetUserReq) (*rpc.GetUserResp, error) {
    user, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
    if err != nil {
        if errors.Is(err, sqlx.ErrNotFound) {
            // 业务错误:用自定义错误码
            return nil, errors.Wrapf(
                xerr.NewErrCode(xerr.USER_NOT_FOUND),
                "用户不存在 userId: %d, err: %v", in.Id, err,
            )
        }
        // 系统错误
        return nil, errors.Wrapf(
            xerr.NewErrCode(xerr.DB_ERROR),
            "查询用户失败 userId: %d, err: %v", in.Id, err,
        )
    }
    return &rpc.GetUserResp{Id: user.Id, Name: user.Name}, nil
}

💡 errors.Wrapf 第 1 参数是给前端的 CodeError,第 2 参数是给日志的详细信息。

gRPC 错误转换拦截器

func ErrorInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    resp, err := handler(ctx, req)
    if err != nil {
        causeErr := errors.Cause(err)  // 解包拿到原始 CodeError
        if e, ok := causeErr.(*xerr.CodeError); ok {
            // 记录完整日志
            logx.WithContext(ctx).Errorf("【RPC-ERR】method: %s, err: %+v", info.FullMethod, err)
            // 转为 gRPC status(错误码透传)
            err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
        } else {
            logx.WithContext(ctx).Errorf("【RPC-ERR】method: %s, err: %+v", info.FullMethod, err)
        }
    }
    return resp, err
}

API 层统一响应

响应结构

// common/xhttp/response.go
package xhttp

type Response struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data,omitempty"`
}

func Success(data interface{}) *Response {
    return &Response{Code: 200, Msg: "OK", Data: data}
}

func Error(code int, msg string) *Response {
    return &Response{Code: code, Msg: msg}
}

核心响应处理函数

func HttpResult(r *http.Request, w http.ResponseWriter, resp interface{}, err error) {
    if err == nil {
        httpx.WriteJson(w, http.StatusOK, Success(resp))
        return
    }

    errcode := int(xerr.SERVER_COMMON_ERROR)
    errmsg  := "服务器开小差啦,稍后再来试一试"

    causeErr := errors.Cause(err)
    if e, ok := causeErr.(*xerr.CodeError); ok {
        // 来自 API 层自己的 CodeError
        errcode = int(e.GetErrCode())
        errmsg  = e.GetErrMsg()
    } else if gstatus, ok := status.FromError(causeErr); ok {
        // 来自 RPC 的 gRPC error(内含自定义错误码)
        grpcCode := uint32(gstatus.Code())
        if xerr.IsCodeErr(grpcCode) {
            errcode = int(grpcCode)
            errmsg  = gstatus.Message()
        }
    }

    logx.WithContext(r.Context()).Errorf("【API-ERR】path: %s, err: %+v", r.URL.Path, err)
    httpx.WriteJson(w, http.StatusOK, Error(errcode, errmsg))
}

💡 注意:HTTP 状态码统一返回 200,业务错误码在 code 字段中区分,供前端精确判断。

参数错误处理

func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {
    errMsg := fmt.Sprintf("%s: %s", xerr.MapErrMsg(xerr.REUQES_PARAM_ERROR), err.Error())
    httpx.WriteJson(w, http.StatusOK, Error(int(xerr.REUQES_PARAM_ERROR), errMsg))
}

在 goctl 模板中注入

修改 ~/.goctl/api/handler.tpl

package handler

import (
    "net/http"
    "your-project/common/xhttp"
    {{.ImportPackages}}
)

func {{.HandlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        {{if .HasRequest}}
        var req types.{{.RequestType}}
        if err := httpx.Parse(r, &req); err != nil {
            xhttp.ParamErrorResult(r, w, err)    // 统一参数错误
            return
        }
        {{end}}
        l := logic.New{{.LogicType}}(r.Context(), ctx)
        resp, err := l.{{.Call}}({{if .HasRequest}}req{{end}})
        xhttp.HttpResult(r, w, resp, err)          // 统一响应格式化
    }
}

💡 定制模板后,所有 goctl api go 生成的 handler 都自动使用统一错误处理。


官方扩展方案(zeromicro/x)

go get github.com/zeromicro/x
import xhttp "github.com/zeromicro/x/http"

// handler 中使用
if err != nil {
    xhttp.Error(w, err)       // → {"code": 1001, "msg": "用户名或密码错误"}
} else {
    xhttp.OkJson(w, resp)     // → {"code": 0, "msg": "ok", "data": {...}}
}

响应格式:

// 成功
{"code": 0, "msg": "ok", "data": {"uid": 1, "name": "go-zero"}}

// 错误
{"code": 1001, "msg": "用户名或密码错误"}

错误码与 HTTP 状态码映射

业务错误码 HTTP 状态码 含义
0 / 200 200 成功
100001 200 服务器内部错误(友好提示)
100002 200 请求参数错误
100003 200 Token 失效
200001 200 用户不存在
200003 200 用户名或密码错误
300003 200 库存不足

💡 统一返回 200 的好处:前端只需关注 code 字段,不需要同时处理 HTTP 状态码和业务错误码。


国际化(i18n)支持

// i18n/messages_zh.go
var zhMessages = map[uint32]string{
    xerr.OK:              "成功",
    xerr.USER_NOT_FOUND:  "用户不存在",
    xerr.WRONG_PASSWORD:  "用户名或密码错误",
}

// i18n/messages_en.go
var enMessages = map[uint32]string{
    xerr.OK:              "Success",
    xerr.USER_NOT_FOUND:  "User not found",
    xerr.WRONG_PASSWORD:  "Invalid username or password",
}

// 根据请求语言选择
func MapErrMsgWithLang(errcode uint32, lang string) string {
    switch lang {
    case "en", "en-US":
        if msg, ok := enMessages[errcode]; ok {
            return msg
        }
    default:
        if msg, ok := zhMessages[errcode]; ok {
            return msg
        }
    }
    return "服务器开小差啦,稍后再来试一试"
}

// 从请求头获取语言
func GetLangFromRequest(r *http.Request) string {
    lang := r.Header.Get("Accept-Language")
    if lang == "" {
        lang = "zh-CN"
    }
    return lang
}

全链路错误传递

Logic 层抛出 CodeError
    ↓
RPC 拦截器捕获 → 转换为 status.Error (gRPC)
    ↓
网络传输(code + msg)
    ↓
API 层 HttpResult 捕获
    → 如果是 gRPC error → 提取业务错误码 + 消息
    → 如果是 CodeError  → 提取业务错误码 + 消息
    → 写入 JSON 响应

最佳实践

实践 说明
错误码用 uint32 兼容 gRPC codes.Code,支持跨服务透传
分层区分 全局/模块/底层分离,一眼看出错误来源
日志与响应分离 errors.Wrapf 第一个参数给前端,第二个给日志
RPC 拦截器统一转换 CodeErrorstatus.Error,不散落在每个 logic 中
HTTP 统一 200 前端只看 code 字段,降低处理复杂度
模板定制 goctl 模板注入统一错误处理,避免手写
区分内部/外部错误 底层错误(DB panic)不暴露给前端

常见陷阱

陷阱 说明
🚨 使用 errors.New 而非 CodeError RPC 拦截器无法识别自定义错误码
🚨 errors.Cause 使用错误 必须用 errors.Cause(err) 解包到最内层,否则类型断言失败
🚨 错误信息暴露内部细节 不要把 DB 连接串、SQL 等敏感信息放在给前端的 errMsg 中
🚨 错误码冲突 不同模块用了相同的错误码,用分层编码规范解决
🚨 不一致的 HTTP 状态码 团队内部约定统一用 200 还是区分 400/401/500