Go Web 小技巧(一)简化Gin接口代码

不知道大家在使用 Gin 构建 API 服务时有没有这样的问题:

  1. 参数绑定的环节可不可以自动处理?
  2. 错误可不可以直接返回,不想写空 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
// User 用户结构
type User struct {
UserName string
}

// CreateUser 创建用户
func CreateUser(ctx *gin.Context) {
var params User
if err := ctx.ShouldBind(&params); 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
// resp.go

// Resp 返回
type Resp struct {
Code int
Msg string
Data 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 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.go
// CreateUser 创建用户
func CreateUser(ctx *gin.Context) {
var params User
if err := ctx.ShouldBind(&params); err != nil {
ErrorResp(ctx, 400, "参数错误")
logrus.Errorf("params err, %v", params)
return
}

// 一些其他的业务逻辑 ...

SuccessResp(ctx, "创建成功")
}

两个痛点

上面的方法还不够完整,我们还是有许多重复的逻辑,可以发现我们在写的绝大多数 API 大概都是这样:

  1. 参数绑定 & 校验
  2. 业务逻辑
  3. 返回

这里面有两个痛点:

  1. 参数绑定的环节可不可以自动处理?
  2. 错误可不可以直接返回,不想写空 return, 漏写就是 bug

    1
    2
    3
    4
    5
    6
    7
    8
    // 不想写大量这种重复的代码
    var params User
    if err := ctx.ShouldBind(&params); 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
// 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.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
// 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 不知道大家发现没有,我们上一次请求的参数会被带到下一次请求当中

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
// 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.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 了,还有很多可以完善的点,这里有一些思路,有的已经做了,有的还在路上

  1. 每次注册都写 Handle(&CreateUser) 还是有点麻烦?

    可以封装一下 gin.IRouter 这个接口,这样注册接口就可以和原来一样了

  2. 参数绑定如果我需要多次绑定怎么办?

    可以添加一个接口,如果实现了这个接口就执行以下,对于有特殊的参数校验之类的也可以采用类似的方式处理

    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)
    }
  3. 怎么输出 API 文档?

    可以和 swagger 之类的 API 文档结合, 利用 go generate 自动生成,顺便可以连接口注册都不用了,添加一行注释,自动注册接口,并且输出接口文档

    1
    2
    // @Router put /api/v1/user
    func(u *User) Request(ctx *gin.Context) (*Resp, error)
  4. 能不能减少 CURD 代码?

    可以实现,只需要采用约定的项目接口,可以 利用 go generate 直接自动生成简单的 CURD 代码

  • 本文作者: mohuishou <1@lailin.xyz>
  • 本文链接: https://lailin.xyz/post/38237.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!