在我们写的绝大部分的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 的方式。