Go语言服务、请求、响应、错误码设计与实现一般的请求响应格

一般的请求响应格式

对于现在大多数系统来说,请求一般就是原始请求参数的JSON格式+HTTP头的Token,响应一般会带上错误码和错误信息,封装为【错误码,错误信息,数据】的JSON格式,请求和响应都会放在HTTP的Body。

对于错误码,常见的选择有数字,字母,和字母+数字这几种方式。

对于HTTP方法,常见的选择有统一POST,获取操作用GET+修改操作用POST,还有RESTful风格的GET、POST、DELETE、PUT等。

对于HTTP状态码,常见的使用方式有统一200,或者使用200、400、401、403、404、500等常见状态码。

对于HTTP头,一般会放置一些权限相关的信息,比如Token。

实现

我们的实现错误码选择字母格式,HTTP状态码使用200、400、500等方便前端对错误进行分组,使用Gin框架实现。

Service格式

Service是业务逻辑实现的地方,在有异常体系的语言里,我们经常使用:服务名(请求) 响应 throws 异常这种结构。但是Go一般使用error表示错误,因此Go的服务结构为:服务名(请求) (响应, 错误)

比如:

type LoginService struct{}

func (s *LoginService) Login(req *model.LoginReq) (*model.LoginRsp, result.Error) {
   if req.Username != "admin" || req.Password != "admin" {
      return nil, result.NewError(result.ErrCodeInvalidParameterUsernameOrPassword)
   }

   return &model.LoginRsp{
      Token: uuid.New().String(),
   }, nil
}
复制代码

其中req和rsp都是简单的结构体:

type LoginReq struct {
   Username string `json:"username"`
   Password string `json:"password"`
}

type LoginRsp struct {
   Token string `json:"token"`
}
复制代码

而Error是一个继承了error接口的接口:

// Unwrap 获取内部异常
type Unwrap interface {
   Unwrap() error
}

// Error 错误
type Error interface {
   error
   Unwrap
   ErrCode
}

// ErrorImpl 错误
type ErrorImpl struct {
   error
   ErrCode
}

// Unwrap 实现 Unwrap 接口
func (e ErrorImpl) Unwrap() error {
   return e.error
}

// WrapError 包装 error
func WrapError(errCode ErrCode, err error) Error {
   return ErrorImpl{
      error:   err,
      ErrCode: errCode,
   }
}

// NewError 新建一个 Error
func NewError(errCode ErrCode) Error {
   return ErrorImpl{
      error:   errors.New(errCode.Msg()),
      ErrCode: errCode,
   }
}
复制代码

这里返回值使用自定义Error接口作为错误而不是go自带的error接口,是因为Error接口可以确保服务在错误情况下一定带上了自定义错误码。

错误码实现

上面的Error接口继承了ErrCode接口,而我们希望错误码能够在系统内部表达具体的错误(错误码+错误信息),对用户表达建议信息,同时能够向前端返回HTTP状态码。因此错误码的接口和实现如下:

var (
   ErrCodeOK               = newErrCode(http.StatusOK, "OK", "", "")
   ErrCodeInvalidParameter = newErrCode(http.StatusBadRequest, "InvalidParameter",
      "The required parameter is not valid.", "非法参数")
   ErrCodeInvalidParameterUsernameOrPassword = newErrCode(http.StatusBadRequest,
      "InvalidParameter.UsernameOrPassword", "The username or password is not valid.", "账号或密码错误")
   ErrCodeInternalError = newErrCode(http.StatusInternalServerError, "InternalError",
      "The request processing has failed due to some unknown error.", "给您带来的不便,深感抱歉,请稍后再试")
)

// ErrCode 错误码
type ErrCode interface {
   Status() int    // HTTP状态码
   Code() string   // 错误码
   Msg() string    // 错误消息
   Advice() string // 建议处理方式
}

// errCodeImpl 错误码实现
type errCodeImpl struct {
   status int
   code   string
   msg    string
   advice string
}

func (e errCodeImpl) Status() int {
   return e.status
}

func (e errCodeImpl) Code() string {
   return e.code
}

func (e errCodeImpl) Msg() string {
   return e.msg
}

func (e errCodeImpl) Advice() string {
   return e.advice
}

// newErrCode 新建一个错误码
func newErrCode(status int, code, msg, advice string) ErrCode {
   return errCodeImpl{
      code:   code,
      msg:    msg,
      status: status,
      advice: advice,
   }
}
复制代码

这里在最上面定义了一些错误码,错误码将全部定义在此文件中

HTTP响应

对于HTTP响应,我们不仅需要带上Service的结果,还需要带上HTTP状态码,错误码,对用户的提示信息。为了简化操作,这里封装了几个工具函数:

// Rsp 响应
type Rsp struct {
   Code string      `json:"code,omitempty"` // 错误码
   Msg  string      `json:"msg,omitempty"`  // 消息
   Data interface{} `json:"data,omitempty"` // 数据
}

// Success 请求成功
func Success(c *gin.Context, data interface{}) {
   rsp(c, ErrCodeOK, data)
}

// Failure 请求失败
func Failure(c *gin.Context, errCode ErrCode) {
   rsp(c, errCode, nil)
}

// rsp 响应
func rsp(c *gin.Context, errCode ErrCode, data interface{}) {
   c.JSON(errCode.Status(), &Rsp{
      Code: errCode.Code(),
      Msg:  errCode.Advice(),
      Data: data,
   })
}
复制代码

不同于Service的Error,HTTP响应的msg使用的是advice,因为这是给用户看的。

Gin HandlerFunc包装

对于一个服务,在使用Gin框架时,如果我们想把它暴露出去,需要编写一个HandlerFunc函数,如:

func LoginHandler(c *gin.Context) {
   // 参数绑定
   var req model.LoginReq
   if err := c.ShouldBindJSON(&req); err != nil {
      result.Failure(c, result.ErrCodeInvalidParameter)
      return
   }

   // 调用服务
   var loginService service.LoginService
   rsp, err := loginService.Login(&req)

   // 结果处理
   if err != nil {
      result.Failure(c, err)
      return
   }
   result.Success(c, rsp)
}
复制代码

但是由于我们的服务、请求、响应、错误码结构都是统一的,对于不同服务的handler,代码除了在请求参数类型不同外,其余都是一样的,这会导致handler的代码非常的冗余,因此我们利用反射机制对Service进行简单的包装,消除对handler的编写:

func WrapService(service interface{}) func(*gin.Context) {
   return func(c *gin.Context) {
      // 参数绑定
      s := reflect.TypeOf(service)
      reqPointType := s.In(0)
      reqStructType := reqPointType.Elem()
      req := reflect.New(reqStructType)
      if err := c.ShouldBindJSON(req.Interface()); err != nil {
         result.Failure(c, result.ErrCodeInvalidParameter)
         return
      }

      // 调用服务
      params := []reflect.Value{reflect.ValueOf(req.Interface())}
      rets := reflect.ValueOf(service).Call(params)

      // 结果处理
      if !rets[1].IsNil() {
         result.Failure(c, rets[1].Interface().(result.Error))
         return
      }
      result.Success(c, rets[0].Interface())
   }
}
复制代码

这样,我们对外暴露服务只需要一行代码:

r.POST("login", WrapService(loginService.Login))
复制代码

全部代码

Github:github.com/XiaoHuaShiF…