Go API 多种响应的规范化处理和简化策略

2 阅读8分钟

一个对外提供API接口的服务,在真正动工开发接口前一般需要先确定一下接口响应的通用格式,无论接口响应里返不返回业务数据,返回的数据是字符串、列表、对象还是其他类型都会遵照这个通用的响应格式。

既然一个项目接口的响应格式是确定的,那么在搭建项目的时候就需要我们提前封装一个通用的接口响应组件,让实现业务逻辑的代码能尽量傻瓜式地调用响应组件,由响应组件负责生成响应返回给客户端。

这篇内容我跟大家一起分析项目接口响应的通用格式应该是什么样的,然后动手为Go项目封装一个统一的接口响应组件让它能为项目生成通用格式的响应,该组件还会对返回分页数据的接口做一个逻辑简化,为错误响应做好兜底。大家跟着我一起来看看吧。

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

图片

请访问xiaobot.net/p/golang 订阅专栏,即可加入实战项目获得配套的完整实战教程

确定项目接口响应的通用格式

一般的响应格式必须有这么几个要素:

  • code : 响应中的业务Code码,一般0表示成功,其他码值会对应到不同的错误上,在《**Go项目Error的统一规划管理策略》中已经教大家怎么按模块管理Error了,响应组件会直接使用那些预定义Error上的code码值作为响应code。
  • msg: 这个好理解就是个信息字符串,有可能前端会以这个值作为客户端的toast 消息。
  • data: 接口中返回的数据,可能是对象也可能是列表,这个就需要负责各个接口的前端组件去对应解析啦
  • request_id: 有的团队会要求返回这个request_id ,不是必须的,但是有它,需要查数据的时候会更好的从日志里回溯请求在服务端都发生了什么。
  • pagination: 接口返回列表数据,有可能需要返回总行数之类的信息,好去请求下一页数据,一般在管理后台类的项目中使用较多, 移动端可能会更喜欢拿数据的last id 去请求下一批数据。

确定好接口响应的通用格式后,接下来我们开始为项目封装响应组件。

封装响应组件

我们先在 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)
}
  • SetPagination 用来设置响应的分页信息
  • Success 返回接口执行符合预期的成功响应,其中会携带Data数据返回给客户端。
  • SuccessOk 针对只需要知道成功状态的接口响应,目的是简化接口程序的调用。在这种情况下不需要使用一个空字符串或者nil参数去调用Success方法。
  • Error 返回错误响应,参数为我们为项目定义的AppError对象,这样响应码使用的既是AppError的Code码,在返回错误响应时会记录一条错误响应,这样即使你在处理程序中没有打错误日志,框架这里也能做个兜底,方便出错后排查问题。

接口响应里的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项目搭建和整洁开发实战》专栏阅读剩余内容。

本专栏力主实战技能,配备完整的实战项目, 订阅入口:xiaobot.net/p/golang

订阅后,可加入专栏配套的实战项目,获得完整实战教程,同时也有专属的读者群,欢迎加入一起学习

专栏分为五大部分 (前三部分25节内容已经更新完成) 主要内容架构如下:

图片

  • 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
  • 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
  • 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
  • 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
  • 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项

访问 xiaobot.net/p/golang 即刻订阅