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

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

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

参数绑定技巧

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

// 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 表

// 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 关联在一起,减少一些结构体转换的代码

// 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 可以避免此类问题

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

4. 多次绑定 request body 数据

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

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 还是会报错的。

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 会导致之后的所有的接口都修改才行,十分的不优雅,下面提供一种不用影响后续使用的方法

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 的情况

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

总结

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

转自:https://zhuanlan.zhihu.com/p/91312616