不知道大家在使用 Gin 構建 API 服務時有沒有這樣的問題:
- 參數綁定的環節可不可以自動處理?
- 錯誤可不可以直接返回,不想寫空
return
, 漏寫就是bug
本文通過簡單地封裝,利用 go 的接口特性,提供一個解決上述兩個問題的思路
一、解決過程
1.1 剛開始時寫 API 服務時
我們剛開始使用 Gin 寫 API 服務時,一般會按照官方文檔上的 這么寫
// User 用戶結構
type User struct {UserName string
}// CreateUser 創建用戶
func CreateUser(ctx *gin.Context) {var params Userif err := ctx.ShouldBind(¶ms); err != nil {ctx.JSON(http.StatusBadRequest, gin.H{"code": 400,"msg": "參數錯誤",})logrus.Errorf("params err, %v", params)return}// 一些其他的業務邏輯 ...ctx.JSON(http.StatusOK, gin.H{"code": 0,"msg": "創建成功",})
}func main() {r := gin.Default()r.POST("user", CreateUser)if err := r.Run(":8080"); err != nil {logrus.Fatalf("can not start serve: %v", err)}
}
1.2 封裝返回值
我們寫了一段時間之后,會發現,我們的返回值的結構是固定的,為什么不抽象一下呢,所以我們創建了一個結構體 Resp
,并且封裝了兩個方法用于成功和失敗這兩種狀態的返回
// resp.go// Resp 返回
type Resp struct {Code intMsg stringData interface{}
}// ErrorResp 錯誤返回值
func ErrorResp(ctx *gin.Context, code int, msg string, data ...interface{}) {resp(ctx, code, msg, data...)
}// SuccessResp 正確返回值
func SuccessResp(ctx *gin.Context, msg string, data ...interface{}) {resp(ctx, 0, msg, data...)
}// resp 返回
func resp(ctx *gin.Context, code int, msg string, data ...interface{}) {resp := Resp{Code: code,Msg: msg,Data: data,}if len(data) == 1 {resp.Data = data[0]}ctx.JSON(http.StatusOK, resp)
}
添加這個方法之后,我們再看一下 CreateUser
這個方法,成功的從 16 行變到了 12 行
// main.go
// CreateUser 創建用戶
func CreateUser(ctx *gin.Context) {var params Userif err := ctx.ShouldBind(¶ms); err != nil {ErrorResp(ctx, 400, "參數錯誤")logrus.Errorf("params err, %v", params)return}// 一些其他的業務邏輯 ...SuccessResp(ctx, "創建成功")
}
1.3 兩個痛點
上面的方法還不夠完整,我們還是有許多重復的邏輯,可以發現我們在寫的絕大多數 API 大概都是這樣:
- 參數綁定 & 校驗
- 業務邏輯
- 返回
這里面有兩個痛點:
- 參數綁定的環節可不可以自動處理?
- 錯誤可不可以直接返回,不想寫空
return
, 漏寫就是bug
// 不想寫大量這種重復的代碼var params Userif err := ctx.ShouldBind(¶ms); err != nil {// 下面這三行是不是可以合并成一行ErrorResp(ctx, 400, "參數錯誤")logrus.Errorf("params err, %v", params)return}
1.4 使用接口封裝請求
上面的這兩個痛點我們可以通過一個輔助函數解決
// Requester 請求
type Requester interface {Request(ctx *gin.Context) (*Resp, error)
}// Handle 請求
func Handle(r Requester) gin.HandlerFunc {return func(ctx *gin.Context) {resp, err := request(r, ctx)if err != nil {var code *errcode.Errorif !errors.As(err, &code) {code = errcode.Unknown.Wrap(err)}resp = &Resp{Code: code.Code,Msg: code.String(),}_ = ctx.Error(err)}ctx.JSON(http.StatusOK, resp)}
}func request(r Requester, ctx *gin.Context) (*controller.Resp, error) {// 參數綁定if err := ctx.ShouldBind(r); err != nil {return nil, errcode.ErrParams.Wrap(err)}return r.Request(ctx)
}
這樣我們只需要實現這個 Requester
, 寫 API 時只需要關注業務邏輯就可以了
// CreateUser 創建用戶
type CreateUser struct {UserName string
}func (u *User) Request(ctx *gin.Context) (*Resp, error) {// 業務邏輯// 返回成功值
}func main() {r := gin.Default()r.POST("user", Handle(&CreateUser))if err := r.Run(":8080"); err != nil {logrus.Fatalf("can not start serve: %v", err)}
}
上面的代碼有一個 bug 不知道大家發現沒有,我們上一次請求的參數會被帶到下一次請求當中
// Handle 請求
func Handle(r Requester) gin.HandlerFunc {return func(ctx *gin.Context) {// 創建一個新的 Requester, 避免將上一次的參數帶到下一次當中if reflect.TypeOf(r).Kind() != reflect.Ptr {panic("must be a pointer")}req := reflect.New(reflect.ValueOf(r).Elem().Type()).Interface().(Requester)resp, err := request(req, ctx)if err != nil {var code *errcode.Errorif !errors.As(err, &code) {code = errcode.Unknown.Wrap(err)}resp = &Resp{Code: code.Code,Msg: code.String(),}_ = ctx.Error(err)}ctx.JSON(http.StatusOK, resp)}
}
二、總結
大概這樣差不多就 ok 了,還有很多可以完善的點,這里有一些思路,有的已經做了,有的還在路上
- 每次注冊都寫
Handle(&CreateUser)
還是有點麻煩?
可以封裝一下 gin.IRouter
這個接口,這樣注冊接口就可以和原來一樣了
2. 參數綁定如果我需要多次綁定怎么辦?
可以添加一個接口,如果實現了這個接口就執行以下,對于有特殊的參數校驗之類的也可以采用類似的方式處理
type Binder interface {Bind(ctx *gin.Context) error}func request(r Requester, ctx *gin.Context) (*controller.Resp, error) {// 參數綁定if err := ctx.ShouldBind(r); err != nil {return nil, errcode.ErrParams.Wrap(err)}// 其余參數綁定if b, ok := r.(Binder); ok {if err := b.Bind(api); err != nil {return nil, errcode.ErrParams.Wrap(err)}}return r.Request(ctx)}
3. 怎么輸出 API 文檔?
可以和 swagger
之類的 API 文檔結合, 利用 go generate
自動生成,順便可以連接口注冊都不用了,添加一行注釋,自動注冊接口,并且輸出接口文檔
// @Router put /api/v1/userfunc(u *User) Request(ctx *gin.Context) (*Resp, error)
4. 能不能減少 CURD 代碼?
可以實現,只需要采用約定的項目接口,可以 利用 go generate
直接自動生成簡單的 CURD 代碼
博客原文
Go Web 小技巧(一)簡化Gin接口代碼?lailin.xyz