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