Go API 多種響應(yīng)的規(guī)范化處理和簡(jiǎn)化策略
一個(gè)對(duì)外提供API接口的服務(wù),在真正動(dòng)工開(kāi)發(fā)接口前一般需要先確定一下接口響應(yīng)的通用格式,無(wú)論接口響應(yīng)里返不返回業(yè)務(wù)數(shù)據(jù),返回的數(shù)據(jù)是字符串、列表、對(duì)象還是其他類型都會(huì)遵照這個(gè)通用的響應(yīng)格式。
既然一個(gè)項(xiàng)目接口的響應(yīng)格式是確定的,那么在搭建項(xiàng)目的時(shí)候就需要我們提前封裝一個(gè)通用的接口響應(yīng)組件,讓實(shí)現(xiàn)業(yè)務(wù)邏輯的代碼能盡量傻瓜式地調(diào)用響應(yīng)組件,由響應(yīng)組件負(fù)責(zé)生成響應(yīng)返回給客戶端。
這篇內(nèi)容我跟大家一起分析項(xiàng)目接口響應(yīng)的通用格式應(yīng)該是什么樣的,然后動(dòng)手為Go項(xiàng)目封裝一個(gè)統(tǒng)一的接口響應(yīng)組件,讓它能為項(xiàng)目生成通用格式的響應(yīng),該組件還會(huì)對(duì)返回分頁(yè)數(shù)據(jù)的接口做一個(gè)邏輯簡(jiǎn)化,為錯(cuò)誤響應(yīng)做好兜底。大家跟著我一起來(lái)看看吧。
圖片
本節(jié)對(duì)應(yīng)的代碼版本為c5,訂閱后加入課程的GitHub項(xiàng)目后可以直接查看本章節(jié)對(duì)應(yīng)的代碼更新
圖片
確定項(xiàng)目接口響應(yīng)的通用格式
一般的響應(yīng)格式必須有這么幾個(gè)要素:
- code : 響應(yīng)中的業(yè)務(wù)Code碼,一般0表示成功,其他碼值會(huì)對(duì)應(yīng)到不同的錯(cuò)誤上,在《Go項(xiàng)目Error的統(tǒng)一規(guī)劃管理策略》中已經(jīng)教大家怎么按模塊管理Error了,響應(yīng)組件會(huì)直接使用那些預(yù)定義Error上的code碼值作為響應(yīng)code。
- msg: 這個(gè)好理解就是個(gè)信息字符串,有可能前端會(huì)以這個(gè)值作為客戶端的toast 消息。
- data: 接口中返回的數(shù)據(jù),可能是對(duì)象也可能是列表,這個(gè)就需要負(fù)責(zé)各個(gè)接口的前端組件去對(duì)應(yīng)解析啦
- request_id: 有的團(tuán)隊(duì)會(huì)要求返回這個(gè)request_id ,不是必須的,但是有它,需要查數(shù)據(jù)的時(shí)候會(huì)更好的從日志里回溯請(qǐng)求在服務(wù)端都發(fā)生了什么。
- pagination: 接口返回列表數(shù)據(jù),有可能需要返回總行數(shù)之類的信息,好去請(qǐng)求下一頁(yè)數(shù)據(jù),一般在管理后臺(tái)類的項(xiàng)目中使用較多, 移動(dòng)端可能會(huì)更喜歡拿數(shù)據(jù)的last id 去請(qǐng)求下一批數(shù)據(jù)。
確定好接口響應(yīng)的通用格式后,接下來(lái)我們開(kāi)始為項(xiàng)目封裝響應(yīng)組件。
封裝響應(yīng)組件
我們先在 common 目錄下新建 app 目錄,其中新增兩個(gè)文件 response.go 和 pagination.go
.
|-- common
| |-- app
| |---pagination.go
| |---response.go
|......
|-- main.go
|-- go.mod
|-- go.sum
在 response.go 定義項(xiàng)目接口的統(tǒng)一響應(yīng)結(jié)構(gòu)
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 是分頁(yè)信息,其結(jié)構(gòu)定義在pagination.go文件中。
type Pagination struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalRows int `json:"total_rows"`
}
reponse定義中 Data 和 Pagination 的結(jié)構(gòu)體 tag 中 都有一個(gè) json:"xxx,omitempty"這個(gè) omitempty 的意思是進(jìn)行JSON格式化的時(shí)候忽略空值。
比如我們的API返回單一的對(duì)象或者不需要分頁(yè)的列表信息時(shí)不會(huì)設(shè)置響應(yīng)的分頁(yè)信息,加上這個(gè)標(biāo)簽后接口的響應(yīng)結(jié)果中就不會(huì)有pagination這個(gè)字段了。data字段也是同一個(gè)道理。
所以我們分別給response定義了 SuccessOk和Success方法,前一個(gè)情況接口程序直接調(diào)用SuccessOk即返回不帶數(shù)據(jù)的成功響應(yīng),后者返回帶數(shù)據(jù)的接口響應(yīng)
我們來(lái)看一下 response 中提供的方法。
// SetPagination 設(shè)置Response的分頁(yè)信息
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
// 兜底記一條響應(yīng)錯(cuò)誤, 項(xiàng)目自定義的AppError中有錯(cuò)誤鏈條, 方便出錯(cuò)后排查問(wèn)題
logger.New(r.ctx).Error("api_response_error", "err", err)
r.ctx.JSON(err.HttpStatusCode(), r)
}
- SetPagination 用來(lái)設(shè)置響應(yīng)的分頁(yè)信息
- Success 返回接口執(zhí)行符合預(yù)期的成功響應(yīng),其中會(huì)攜帶Data數(shù)據(jù)返回給客戶端。
- SuccessOk 針對(duì)只需要知道成功狀態(tài)的接口響應(yīng),目的是簡(jiǎn)化接口程序的調(diào)用。在這種情況下不需要使用一個(gè)空字符串或者nil參數(shù)去調(diào)用Success方法。
- Error 返回錯(cuò)誤響應(yīng),參數(shù)為我們?yōu)轫?xiàng)目定義的AppError對(duì)象,這樣響應(yīng)碼使用的既是AppError的Code碼,在返回錯(cuò)誤響應(yīng)時(shí)會(huì)記錄一條錯(cuò)誤響應(yīng),這樣即使你在處理程序中沒(méi)有打錯(cuò)誤日志,框架這里也能做個(gè)兜底,方便出錯(cuò)后排查問(wèn)題。
接口響應(yīng)里的requestId 我們?nèi)〉氖钱?dāng)次請(qǐng)求對(duì)應(yīng)的tracceid這樣requestId 也能跟我們本次請(qǐng)求的所有日志中攜帶的traceid 對(duì)應(yīng)起來(lái),具體可參前面的文章Go日志門面的設(shè)計(jì)與實(shí)現(xiàn)-自動(dòng)注入追蹤ID。
用組件返回成功和錯(cuò)誤響應(yīng)
接下來(lái)我們?cè)陧?xiàng)目中寫幾個(gè)簡(jiǎn)單的接口測(cè)試一下組件的功能。
先寫一個(gè)返回返回對(duì)象信息的測(cè)試接口。
g.GET("/response-obj", func(c *gin.Context) {
data := map[string]int{
"a": 1,
"b": 2,
}
app.NewResponse(c).Success(data)
return
})
運(yùn)行項(xiàng)目后訪問(wèn)接口會(huì)看到以下結(jié)果。
圖片
再來(lái)一個(gè)返回錯(cuò)誤響應(yīng)的測(cè)試接口。
g.GET("/response-error", func(c *gin.Context) {
baseErr := errors.New("a dao error")
// 這一步正式開(kāi)發(fā)時(shí)寫在service層
err := errcode.Wrap("encountered an error when xxx service did xxx", baseErr)
app.NewResponse(c).Error(errcode.ErrServer.WithCause(err))
return
})
這里是Mock了一個(gè)錯(cuò)誤進(jìn)行了返回,運(yùn)行項(xiàng)目訪問(wèn)接口會(huì)看到下面的結(jié)果
圖片
返回錯(cuò)誤響應(yīng)時(shí),我并沒(méi)有記錯(cuò)誤日志,但是的組件會(huì)幫我們兜底記了一條響應(yīng)錯(cuò)誤的日志, 防止開(kāi)發(fā)中忘了在程序中打錯(cuò)誤日志。