当前位置:首页 > Golang > Gin框架参数多次绑定

宝塔服务器面板,一键全能部署及管理,送你3188元礼包,点我领取

Gin框架参数多次绑定

转载自:知乎 2022-12-05 Golang 157
分享给朋友:

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

  1. 我创建/更新接口有时候就仅仅只相差一个 id,我是不是可以复用代码?

  2. 是否可以直接用 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 的方式。

分享给朋友:

相关文章

手摸手源码单机部署OpenIM实况教学

该操作均在CentOS7下进行这里不推荐直接使用 git clone --recursive 方式,因为大家都懂的网络问题!首先到 https://github.com/OpenIMSDK/Open-IM-Server 下载项目压缩包再到 https

一个简单的Golang代码,监控windows进程退出并自动重启

只要修改processName 为你所需要的进程exeprocessName_path 为你所需要的进程路径package main import (     "bytes"   

gocron数据库表和字段字符集转换为utf8mb4

gocron_sql_convert_to_utf8mb4因为 gocron - 定时任务管理系统 默认表和字段字符集是utf8(也就是utf8mb3),所以在现下很多数据存储会有问题,最为突出的就是emoji表情写了一个简单的转换sql:git地址 有用到的小伙伴可以拿去直接转换

如何在Linux上做一个定时任务管理系统

gocron,使用Go语言开发的轻量级定时任务集中调度和管理系统, 用于替代Linux-crontab

微软推出Go语言从入门到上手的文档

微软有自己的技术文档专栏网站,可能很多小伙伴都不知道!除了自己的.NET语言,还包括了市面上绝大多数流行语言的技术文档都有。这次,微软把Go语言也加入到了自家技术文档网站中,而且绝大多数文档都是有中文版本的!点击直接访问:更多文档访问以下地址:https://docs.microsoft.com/z

go语言之gin框架初尝试

对于Webservice服务,几乎所有的企业都在使用restful风格的API。Gin也天然的支持restful。gin框架虽然不大,但是功能却不少。首先先配置好go环境,针对不同系统go的配置环境可能不一样。