一个对外提供API接口的服务,在真正动工开发接口前一般需要先确定一下接口响应的通用格式,无论接口响应里返不返回业务数据,返回的数据是字符串、列表、对象还是其他类型都会遵照这个通用的响应格式。
既然一个项目接口的响应格式是确定的,那么在搭建项目的时候就需要我们提前封装一个通用的接口响应组件,让实现业务逻辑的代码能尽量傻瓜式地调用响应组件,由响应组件负责生成响应返回给客户端。
这篇内容我跟大家一起分析项目接口响应的通用格式应该是什么样的,然后动手为Go项目封装一个统一的接口响应组件,让它能为项目生成通用格式的响应,该组件还会对返回分页数据的接口做一个逻辑简化,为错误响应做好兜底。大家跟着我一起来看看吧。
图片
本节对应的代码版本为c5,订阅后加入课程的GitHub项目后可以直接查看本章节对应的代码更新
图片
确定项目接口响应的通用格式
一般的响应格式必须有这么几个要素:
- 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了一个错误进行了返回,运行项目访问接口会看到下面的结果
图片
返回错误响应时,我并没有记错误日志,但是的组件会帮我们兜底记了一条响应错误的日志, 防止开发中忘了在程序中打错误日志。