模型绑定和验证

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 推断出绑定器,如果你明确你要绑定内容,可以使用 MustBindWithShouldBindWith
你也可以指定必填的字段。如果一个字段使用 binding:"required" 修饰,并且被绑定到一个空值的时候,将会返回一个错误。

  1. // 从 JSON 绑定
  2. type Login struct {
  3. User string `form:"user" json:"user" binding:"required"`
  4. // password的长度必须为10
  5. Password string `form:"password" json:"password" binding:"required,len = 10"`
  6. }
  7. func main() {
  8. router := gin.Default()
  9. // 绑定 JSON 的示例 ({"user": "manu", "password": "123"})
  10. router.POST("/loginJSON", func(c *gin.Context) {
  11. var json Login
  12. if err := c.ShouldBindJSON(&json); err == nil {
  13. if json.User == "manu" && json.Password == "123" {
  14. c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
  15. } else {
  16. c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
  17. }
  18. } else {
  19. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  20. }
  21. })
  22. // 一个 HTML 表单绑定的示例 (user=manu&password=123)
  23. router.POST("/loginForm", func(c *gin.Context) {
  24. var form Login
  25. // 这个将通过 content-type 头去推断绑定器使用哪个依赖。
  26. if err := c.ShouldBind(&form); err == nil {
  27. if form.User == "manu" && form.Password == "123" {
  28. c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
  29. } else {
  30. c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
  31. }
  32. } else {
  33. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  34. }
  35. })
  36. // 监听并服务于 0.0.0.0:8080
  37. router.Run(":8080")
  38. }

跳过验证

当在命令行上使用 curl 运行上面的示例时,它会返回一个错误。因为示例给 Password 绑定了 binding:"required" 。如果 Password 使用 binding:"-" ,然后再次运行上面的示例,它将不会返回错误。

自定义验证器

可以注册自定义验证器。 参见 示例代码

  1. package main
  2. import (
  3. "net/http"
  4. "reflect"
  5. "time"
  6. "github.com/gin-gonic/gin"
  7. "github.com/gin-gonic/gin/binding"
  8. "gopkg.in/go-playground/validator.v8"
  9. )
  10. type Booking struct {
  11. CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
  12. CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
  13. }
  14. func bookableDate(
  15. v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
  16. field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
  17. ) bool {
  18. if date, ok := field.Interface().(time.Time); ok {
  19. today := time.Now()
  20. if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
  21. return false
  22. }
  23. }
  24. return true
  25. }
  26. func main() {
  27. route := gin.Default()
  28. if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  29. v.RegisterValidation("bookabledate", bookableDate)
  30. }
  31. route.GET("/bookable", getBookable)
  32. route.Run(":8085")
  33. }
  34. func getBookable(c *gin.Context) {
  35. var b Booking
  36. if err := c.ShouldBindWith(&b, binding.Query); err == nil {
  37. c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
  38. } else {
  39. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  40. }
  41. }

结构级别的验证 可以像这样注册。
查看 示例 struct-lvl-validation 学习更多。

只绑定查询字符串

ShouldBindQuery 函数只绑定查询参数而不绑定 post 数据。查看 详细信息.

  1. package main
  2. import (
  3. "log"
  4. "github.com/gin-gonic/gin"
  5. )
  6. type Person struct {
  7. Name string `form:"name"`
  8. Address string `form:"address"`
  9. }
  10. func main() {
  11. route := gin.Default()
  12. route.Any("/testing", startPage)
  13. route.Run(":8085")
  14. }
  15. func startPage(c *gin.Context) {
  16. var person Person
  17. if c.ShouldBindQuery(&person) == nil {
  18. log.Println("====== Only Bind By Query String ======")
  19. log.Println(person.Name)
  20. log.Println(person.Address)
  21. }
  22. c.String(200, "Success")
  23. }

绑定查询字符串或 post 数据

查看 详细信息.

  1. package main
  2. import "log"
  3. import "github.com/gin-gonic/gin"
  4. import "time"
  5. type Person struct {
  6. Name string `form:"name"`
  7. Address string `form:"address"`
  8. Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
  9. }
  10. func main() {
  11. route := gin.Default()
  12. route.GET("/testing", startPage)
  13. route.Run(":8085")
  14. }
  15. func startPage(c *gin.Context) {
  16. var person Person
  17. // 如果是 `GET`, 只使用 `Form` 绑定引擎 (`query`) 。
  18. // 如果 `POST`, 首先检查 `content-type` 为 `JSON` 或 `XML`, 然后使用 `Form` (`form-data`)
  19. // 需要注意的是,如果同时给了json与form,`form:"name" json:"name"`,会以json为准
  20. // 在这里查看更多信息 https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
  21. if c.ShouldBind(&person) == nil {
  22. log.Println(person.Name)
  23. log.Println(person.Address)
  24. log.Println(person.Birthday)
  25. }
  26. c.String(200, "Success")
  27. }

绑定uri

https://github.com/gin-gonic/gin/issues/846

  1. package main
  2. import "github.com/gin-gonic/gin"
  3. type Person struct {
  4. ID string `uri:"id" binding:"required,uuid"`
  5. Name string `uri:"name" binding:"required"`
  6. }
  7. func main() {
  8. route := gin.Default()
  9. route.GET("/:name/:id", func(c *gin.Context) {
  10. var person Person
  11. if err := c.ShouldBindUri(&person); err != nil {
  12. c.JSON(400, gin.H{"msg": err})
  13. return
  14. }
  15. c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
  16. })
  17. route.Run(":8088")
  18. }
  19. ok ----localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
  20. notok ----localhost:8088/thinkerou/not-uuid

Multipart/Urlencoded绑定

  1. type ProfileForm struct {
  2. Name string `form:"name" binding:"required"`
  3. Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
  4. // or for multiple files
  5. // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
  6. }
  7. func main() {
  8. router := gin.Default()
  9. router.POST("/profile", func(c *gin.Context) {
  10. var form ProfileForm
  11. if err := c.ShouldBind(&form); err != nil {
  12. c.String(http.StatusBadRequest, "bad request")
  13. return
  14. }
  15. err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
  16. if err != nil {
  17. c.String(http.StatusInternalServerError, "unknown error")
  18. return
  19. }
  20. c.String(http.StatusOK, "ok")
  21. })
  22. router.Run(":8080")
  23. }

备注:这里使用,如下的结构体是会报错的

  1. type CreateNewsReqDto struct {
  2. NewsId int64 `form:"newsId" json:"newsId"` // 新闻ID
  3. File *multipart.Form `form:"file" json:"file" binding:"required"` //上传得文件
  4. }

看源码https://github.com/gin-gonic/gin/blob/master/binding/multipart_form_mapping.go
只支持multipart.FileHeader 或 *multipart.FileHeader的绑定

  1. ype multipartRequest http.Request
  2. var _ setter = (*multipartRequest)(nil)
  3. // TrySet tries to set a value by the multipart request with the binding a form file
  4. func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
  5. if files := r.MultipartForm.File[key]; len(files) != 0 {
  6. return setByMultipartFormFile(value, field, files)
  7. }
  8. return setByForm(value, field, r.MultipartForm.Value, key, opt)
  9. }
  10. func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
  11. switch value.Kind() {
  12. case reflect.Ptr:
  13. switch value.Interface().(type) {
  14. case *multipart.FileHeader:
  15. value.Set(reflect.ValueOf(files[0]))
  16. return true, nil
  17. }
  18. case reflect.Struct:
  19. switch value.Interface().(type) {
  20. case multipart.FileHeader:
  21. value.Set(reflect.ValueOf(*files[0]))
  22. return true, nil
  23. }
  24. case reflect.Slice:
  25. slice := reflect.MakeSlice(value.Type(), len(files), len(files))
  26. isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
  27. if err != nil || !isSetted {
  28. return isSetted, err
  29. }
  30. value.Set(slice)
  31. return true, nil
  32. case reflect.Array:
  33. return setArrayOfMultipartFormFiles(value, field, files)
  34. }
  35. return false, errors.New("unsupported field type for multipart.FileHeader")
  36. }
  37. func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
  38. if value.Len() != len(files) {
  39. return false, errors.New("unsupported len of array for []*multipart.FileHeader")
  40. }
  41. for i := range files {
  42. setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
  43. if err != nil || !setted {
  44. return setted, err
  45. }
  46. }
  47. return true, nil
  48. }

Bind Header

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/gin-gonic/gin"
  5. )
  6. type testHeader struct {
  7. Rate int `header:"Rate"`
  8. Domain string `header:"Domain"`
  9. }
  10. func main() {
  11. r := gin.Default()
  12. r.GET("/", func(c *gin.Context) {
  13. h := testHeader{}
  14. if err := c.ShouldBindHeader(&h); err != nil {
  15. c.JSON(200, err)
  16. }
  17. fmt.Printf("%#v\n", h)
  18. c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
  19. })
  20. r.Run()
  21. // client
  22. // curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
  23. // output
  24. // {"Domain":"music","Rate":300}
  25. }