注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
不知道大家在使用 Gin 构建 API 服务时有没有这样的问题:
- 参数绑定的环节可不可以自动处理?
- 错误可不可以直接返回,不想写空
return
, 漏写就是 bug
本文通过简单地封装,利用 go 的接口特性,提供一个解决上述两个问题的思路
解决过程
刚开始时写 API 服务时
我们刚开始使用 Gin 写 API 服务时,一般会按照官方文档上的 🌰 这么写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| type User struct { UserName string }
func CreateUser(ctx *gin.Context) { var params User if 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) } }
|
封装返回值
我们写了一段时间之后,会发现,我们的返回值的结构是固定的,为什么不抽象一下呢,所以我们创建了一个结构体 Resp
,并且封装了两个方法用于成功和失败这两种状态的返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
type Resp struct { Code int Msg string Data interface{} }
func ErrorResp(ctx *gin.Context, code int, msg string, data ...interface{}) { resp(ctx, code, msg, data...) }
func SuccessResp(ctx *gin.Context, msg string, data ...interface{}) { resp(ctx, 0, msg, data...) }
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 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
func CreateUser(ctx *gin.Context) { var params User if err := ctx.ShouldBind(¶ms); err != nil { ErrorResp(ctx, 400, "参数错误") logrus.Errorf("params err, %v", params) return }
SuccessResp(ctx, "创建成功") }
|
两个痛点
上面的方法还不够完整,我们还是有许多重复的逻辑,可以发现我们在写的绝大多数 API 大概都是这样:
- 参数绑定 & 校验
- 业务逻辑
- 返回
这里面有两个痛点:
参数绑定的环节可不可以自动处理?
错误可不可以直接返回,不想写空 return
, 漏写就是 bug
1 2 3 4 5 6 7 8
| var params User if err := ctx.ShouldBind(¶ms); err != nil { ErrorResp(ctx, 400, "参数错误") logrus.Errorf("params err, %v", params) return }
|
使用接口封装请求
上面的这两个痛点我们可以通过一个辅助函数解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| type Requester interface { Request(ctx *gin.Context) (*Resp, error) }
func Handle(r Requester) gin.HandlerFunc { return func(ctx *gin.Context) { resp, err := request(r, ctx) if err != nil { var code *errcode.Error if !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 时只需要关注业务逻辑就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 不知道大家发现没有,我们上一次请求的参数会被带到下一次请求当中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func Handle(r Requester) gin.HandlerFunc { return func(ctx *gin.Context) { 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.Error if !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
这个接口,这样注册接口就可以和原来一样了
参数绑定如果我需要多次绑定怎么办?
可以添加一个接口,如果实现了这个接口就执行以下,对于有特殊的参数校验之类的也可以采用类似的方式处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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) }
|
怎么输出 API 文档?
可以和 swagger
之类的 API 文档结合, 利用 go generate
自动生成,顺便可以连接口注册都不用了,添加一行注释,自动注册接口,并且输出接口文档
1 2
| func(u *User) Request(ctx *gin.Context) (*Resp, error)
|
能不能减少 CURD 代码?
可以实现,只需要采用约定的项目接口,可以 利用 go generate
直接自动生成简单的 CURD 代码
关注我获取更新
猜你喜欢