
一个对外提供API接口的服务,在真正动工开发接口前一般需要先确定一下接口响应的通用格式,无论接口响应里返不返回业务数据,返回的数据是字符串、列表、对象还是其他类型都会遵照这个通用的响应格式。
既然一个项目接口的响应格式是确定的,那么在搭建项目的时候就需要我们提前封装一个通用的接口响应组件,让实现业务逻辑的代码能尽量傻瓜式地调用响应组件,由响应组件负责生成响应返回给客户端。
这篇内容我跟大家一起分析项目接口响应的通用格式应该是什么样的,然后动手为Go项目封装一个统一的接口响应组件,让它能为项目生成通用格式的响应,该组件还会对返回分页数据的接口做一个逻辑简化,为错误响应做好兜底。大家跟着我一起来看看吧。

本节对应的代码版本为c5,订阅后加入课程的GitHub项目后可以直接查看本章节对应的代码更新

一般的响应格式必须有这么几个要素:
确定好接口响应的通用格式后,接下来我们开始为项目封装响应组件。
我们先在 common 目录下新建 app 目录,其中新增两个文件 response.go 和 pagination.go
.
|-- common
|   |-- app
|       |---pagination.go
|       |---response.go
|......
|-- main.go
|-- go.mod
|-- go.sum
在 response.go 定义项目接口的统一响应结构
type response struct {
 ctx        *gin.Context
 Code       int         `json:"code"`
 Msg        string      `json:"msg"`
 RequestId  string      `json:"request_id"`
 Data       interface{} `json:"data,omitempty"`
 Pagination *Pagination `json:"pagination,omitempty"`
}
response 中的 Pagination 是分页信息,其结构定义在pagination.go文件中。
type Pagination struct {
    Page      int `json:"page"`
    PageSize  int `json:"page_size"`
    TotalRows int `json:"total_rows"`
}
reponse定义中 Data 和 Pagination 的结构体 tag 中 都有一个 json:"xxx,omitempty"这个 omitempty 的意思是进行JSON格式化的时候忽略空值。
比如我们的API返回单一的对象或者不需要分页的列表信息时不会设置响应的分页信息,加上这个标签后接口的响应结果中就不会有pagination这个字段了。data字段也是同一个道理。
所以我们分别给response定义了 SuccessOk和Success方法,前一个情况接口程序直接调用SuccessOk即返回不带数据的成功响应,后者返回带数据的接口响应
我们来看一下 response 中提供的方法。
// SetPagination 设置Response的分页信息
func (r *response) SetPagination(pagination *Pagination) *response {
 r.Pagination = pagination
 return r
}
func (r *response) Success(data interface{}) {
 r.Code = errcode.Success.Code()
 r.Msg = errcode.Success.Msg()
 requestId := ""
 if _, exists := r.ctx.Get("traceid"); exists {
  val, _ := r.ctx.Get("traceid")
  requestId = val.(string)
 }
 r.RequestId = requestId
 r.Data = data
 r.ctx.JSON(errcode.Success.HttpStatusCode(), r)
}
func (r *response) SuccessOk() {
 r.Success("")
}
func (r *response) Error(err *errcode.AppError) {
 r.Code = err.Code()
 r.Msg = err.Msg()
 requestId := ""
 if _, exists := r.ctx.Get("traceid"); exists {
  val, _ := r.ctx.Get("traceid")
  requestId = val.(string)
 }
 r.RequestId = requestId
 // 兜底记一条响应错误, 项目自定义的AppError中有错误链条, 方便出错后排查问题
 logger.New(r.ctx).Error("api_response_error", "err", err)
 r.ctx.JSON(err.HttpStatusCode(), r)
}
接口响应里的requestId 我们取的是当次请求对应的tracceid这样requestId 也能跟我们本次请求的所有日志中携带的traceid 对应起来,具体可参前面的文章Go日志门面的设计与实现-自动注入追踪ID。
接下来我们在项目中写几个简单的接口测试一下组件的功能。
先写一个返回返回对象信息的测试接口。
 g.GET("/response-obj", func(c *gin.Context) {
  data := map[string]int{
   "a": 1,
   "b": 2,
  }
  app.NewResponse(c).Success(data)
  return
 })
运行项目后访问接口会看到以下结果。

再来一个返回错误响应的测试接口。
 g.GET("/response-error", func(c *gin.Context) {
  baseErr := errors.New("a dao error")
  // 这一步正式开发时写在service层
  err := errcode.Wrap("encountered an error when xxx service did xxx", baseErr)
  app.NewResponse(c).Error(errcode.ErrServer.WithCause(err))
  return
 })
这里是Mock了一个错误进行了返回,运行项目访问接口会看到下面的结果

返回错误响应时,我并没有记错误日志,但是的组件会帮我们兜底记了一条响应错误的日志, 防止开发中忘了在程序中打错误日志。

结合我们在《学会定制化 Go 项目的 error,回溯错误的原因和发生位置》给项目Error增加了错误原因链和发生位置记录的功能,这样一来,即使你在开发过程中全程都没有打日志,也不至于出问题后查不到相关的信息。
接下来组件在返回分页数据时怎么简化项目中分页的代码逻辑,请订阅《Go项目搭建和整洁开发实战》专栏阅读剩余内容。