模型绑定和验证
Gin 使用 go-playground/validator.v8 进行验证。 在 这里 查看标签使用的完整文档。
- 类型- Must bind
- 方法 -
Bind
,BindJSON
,BindQuery
- 行为 - 这些方法在底层使用
MustBindWith
。如果存在绑定错误, 请求通过c.AbortWithError(400, err).SetType(ErrorTypeBind)
被终止。 这组响应的状态吗被设置成 400 ,并将Content-Type
头设置成text/plain; charset=utf-8
。注意: 如果你尝试在这个之后去设置响应码,它会发出一个警告[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
。 如果你希望更好的控制行为, 请考虑使用ShouldBind
等效的方法。
- 方法 -
- 类型- Should bind
- 方法 -
ShouldBind
,ShouldBindJSON
,ShouldBindQuery
- 行为 - 这些方法在底层使用
ShouldBindWith
。 如果存在绑定错误,这个错误会被返回, 需要开发者去处理相应的请求和错误。
- 方法 -
使用 Bind 系列方法时, Gin 会尝试通过 Content-Type 推断出绑定器,如果你明确你要绑定内容,可以使用 MustBindWith
或 ShouldBindWith
。
你也可以指定必填的字段。如果一个字段使用 binding:"required"
修饰,并且被绑定到一个空值的时候,将会返回一个错误。
// 从 JSON 绑定
type Login struct {
User string `form:"user" json:"user" binding:"required"`
// password的长度必须为10
Password string `form:"password" json:"password" binding:"required,len = 10"`
}
func main() {
router := gin.Default()
// 绑定 JSON 的示例 ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err == nil {
if json.User == "manu" && json.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 一个 HTML 表单绑定的示例 (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// 这个将通过 content-type 头去推断绑定器使用哪个依赖。
if err := c.ShouldBind(&form); err == nil {
if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 监听并服务于 0.0.0.0:8080
router.Run(":8080")
}
跳过验证
当在命令行上使用 curl
运行上面的示例时,它会返回一个错误。因为示例给 Password
绑定了 binding:"required"
。如果 Password
使用 binding:"-"
,然后再次运行上面的示例,它将不会返回错误。
自定义验证器
可以注册自定义验证器。 参见 示例代码 。
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}
func main() {
route := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
结构级别的验证 可以像这样注册。
查看 示例 struct-lvl-validation 学习更多。
只绑定查询字符串
ShouldBindQuery
函数只绑定查询参数而不绑定 post 数据。查看 详细信息.
package main
import (
"log"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
绑定查询字符串或 post 数据
查看 详细信息.
package main
import "log"
import "github.com/gin-gonic/gin"
import "time"
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// 如果是 `GET`, 只使用 `Form` 绑定引擎 (`query`) 。
// 如果 `POST`, 首先检查 `content-type` 为 `JSON` 或 `XML`, 然后使用 `Form` (`form-data`)
// 需要注意的是,如果同时给了json与form,`form:"name" json:"name"`,会以json为准
// 在这里查看更多信息 https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}
c.String(200, "Success")
}
绑定uri
https://github.com/gin-gonic/gin/issues/846
package main
import "github.com/gin-gonic/gin"
type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
route.Run(":8088")
}
ok ----localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
notok ----localhost:8088/thinkerou/not-uuid
Multipart/Urlencoded绑定
type ProfileForm struct {
Name string `form:"name" binding:"required"`
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
// or for multiple files
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/profile", func(c *gin.Context) {
var form ProfileForm
if err := c.ShouldBind(&form); err != nil {
c.String(http.StatusBadRequest, "bad request")
return
}
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
if err != nil {
c.String(http.StatusInternalServerError, "unknown error")
return
}
c.String(http.StatusOK, "ok")
})
router.Run(":8080")
}
备注:这里使用,如下的结构体是会报错的
type CreateNewsReqDto struct {
NewsId int64 `form:"newsId" json:"newsId"` // 新闻ID
File *multipart.Form `form:"file" json:"file" binding:"required"` //上传得文件
}
看源码https://github.com/gin-gonic/gin/blob/master/binding/multipart_form_mapping.go
只支持multipart.FileHeader 或 *multipart.FileHeader的绑定
ype multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
}
}
return true, nil
}
Bind Header
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
r.Run()
// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}