Go API 多種響應的規范化處理和簡化策略
一個對外提供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了一個錯誤進行了返回,運行項目訪問接口會看到下面的結果
圖片
返回錯誤響應時,我并沒有記錯誤日志,但是的組件會幫我們兜底記了一條響應錯誤的日志, 防止開發中忘了在程序中打錯誤日志。