Go Web 小技巧(三)Gin 参数绑定

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

楔(xiē)子

在第一篇文章 简化 Gin 接口代码 当中为大家提供了一种抽象 gin 接口代码的思路,而这篇文章会为大家带来参数绑定的一些技巧。

在我们写的绝大部分的 API 代码当中,其实都是需要传递参数的,无论是通过 pathquery string 还是 body,在 gin 当中,为我们提供了一系列的 binding 方法让我们可以把这些参数绑定到一个对象中,通过还可以通过 struct tag 来对参数进行校验,不知道到大家曾今是否和遇到过相同的困惑:

  • 我创建/更新接口有时候就仅仅只相差一个 id,我是不是可以复用代码?
  • 是否可以直接用 model 层的 struct 绑定参数?

接下来本文就从这些问题出发,利用 go 的组合特点,介绍一些参数绑定上的小技巧

参数绑定技巧

1. 复用创建/更新时的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UserParams 用户参数
type UserParams struct {
Name string `json:"name" binding:"required"`
Password string `json:"password" binding:"required"`
}

// CreateUserParams 创建用户
type CreateUserParams struct {
UserParams
}

// UpdateUserParams 更新用户
type UpdateUserParams struct {
UserParams
ID uint `json:"id" binding:"required"`
}

2. 用 model 层的 struct 绑定参数

如果我们在参数绑定的时候,向上面那样,每个参数单独创建一个绑定,这样在 Model 层创建数据库记录的时候就需要去手动的转换一道了,如果每个都需要这么写,会感觉很麻烦。

这是原本的 user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// model/model.go

// Model default model
type Model struct {
ID uint `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at" sql:"index"`
}

// model/user.go

// User 用户表
type User struct {
Model

Name string `json:"name"`
Password string `json:"password"`
}

这时我们可以稍微改造一下, 利用 binding:"-" 忽略字段的功能,和 struct 组合,将 api 的参数和 model 关联在一起,减少一些结构体转换的代码

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
35
36
// model/model.go

// Model default model
type Model struct {
ID uint `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" binding:"-"`
UpdatedAt time.Time `json:"updated_at" binding:"-"`
DeletedAt *time.Time `json:"deleted_at" sql:"index" binding:"-"`
}

// model/user.go

// User 用户表
type User struct {
Model

Name string `json:"name" binding:"required"`
Password string `json:"password" binding:"required"`
}

// api/user.go
// UserParams 用户参数
type UserParams struct {
model.User
}

// CreateUserParams 创建用户
type CreateUserParams struct {
UserParams
ID uint `json:"id" gorm:"primary_key" binding:"-"`
}

// UpdateUserParams 更新用户
type UpdateUserParams struct {
UserParams
}

3. 使用 ShouldBind 而不是 Bind

Bind 方法会自动将 http status 设置为 400, 然后报错,但是我们往往会需要携带更多的信息返回,或者返回不同的 status 这时候往往会出现下面这样的警告,而使用 ShouldBind 可以避免此类问题

1
[WARNING] Headers were already written. Wanted to override status code 400 with 200

4. 多次绑定 request body 数据

这是官方文档的一个示例,一般情况第二次读取 request body 的数据就会出现 EOF 的错误,因为 c.Request.Body 不可以重用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type formA struct {
Foo string `json:"foo" binding:"required"`
}

type formB struct {
Bar string `json:"bar" binding:"required"`
}

func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// c.ShouldBind 使用了 c.Request.Body,不可重用。
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 因为现在 c.Request.Body 是 EOF,所以这里会报错。
} else if errB := c.ShouldBind(&objB); errB == nil {
c.String(http.StatusOK, `the body should be formB`)
} else {
...
}
}

gin 1.4 之后官方提供了一个 ShouldBindBodyWith 的方法,可以支持重复绑定,原理就是将 body 的数据缓存了下来,但是二次取数据的时候还是得用 ShouldBindBodyWith 才行,直接用 ShouldBind 还是会报错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// 读取 c.Request.Body 并将结果存入上下文。
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// 这时, 复用存储在上下文中的 body。
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// 可以接受其他格式
} else {
...
}
}

这种方式其实有个问题,什么情况下我们会去多次读取 body 的数据,其实在中间件中我们是需要用到,有的中间件需要读取参数做一些处理,例如权限中间件需要获取当前资源的 id,判断当前用户是否有权限,如果这个时候直接使用 ShouldBindBodyWith 会导致之后的所有的接口都修改才行,十分的不优雅,下面提供一种不用影响后续使用的方法

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
func startPage(c *gin.Context) {
var (
p1 Person
p2 Person
)
buf := &bytes.Buffer{}
tea := io.TeeReader(c.Request.Body, buf)
body, err := ioutil.ReadAll(tea)
if err != nil {
log.Panicf("read body err: %+v", err)
}
c.Request.Body = ioutil.NopCloser(buf)
// read buf ...
if err := binding.JSON.BindBody(body, &p1); err != nil {
log.Panic("p1", err)
}
log.Println("p1", p1)
// use ShouldBind again ..
if err := c.ShouldBind(&p2); err != nil {
log.Panic("p2", err)
}
log.Println("p2", p2)
c.String(200, "Success")
}


// output
// 2019/11/06 23:10:04 p1 {hello world}
// 2019/11/06 23:10:04 p2 {hello world}
// [GIN] 2019/11/06 - 23:10:04 | 200 | 27.0400917s | ::1 | POST /testing

这三行也适用于其他需要多次读取 io.Reader 的情况

1
2
3
buf := &bytes.Buffer{}
tea := io.TeeReader(c.Request.Body, buf)
body, err := ioutil.ReadAll(tea)

总结

这篇文章从参数绑定的问题出发,为大家介绍了两种组合参数的小技巧,提供了使用参数绑定的时候的一个建议,并且提出了非官方,侵入性小的的多次读取 request body 的方式。

下一篇文章给大家介绍 gorm 事务的一些小技巧

参考资料

  1. gin 官方文档

  2. How to read multiple times from same io.Reader

关注我获取更新

wechat
知乎
github

猜你喜欢