在做API部分开发时,需要对请求参数的校验,防止用户的恶意请求。
例如日期格式,用户年龄,性别等必须是正常的值,不能随意设置。
我们使用github.com/go-playground/validator进行参数校验
只需要在定义结构体时使用binding或validatetag标识相关校验规则

1 安装与使用

go get github.com/go-playground/validator/v10

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/go-playground/validator/v10"
  5. )
  6. type User struct {
  7. Username string `validate:"min=6,max=10"`
  8. Age uint8 `validate:"gte=1,lte=10"`
  9. Sex string `validate:"oneof=female male"`
  10. }
  11. func main() {
  12. validate := validator.New()
  13. user1 := User{Username: "aaa", Age: 11, Sex: "null"}
  14. err := validate.Struct(user1)
  15. if err != nil {
  16. fmt.Println(err)
  17. }
  18. user2 := User{Username: "bbb", Age: 8, Sex: "male"}
  19. err = validate.Struct(user2)
  20. if err != nil {
  21. fmt.Println(err)
  22. }
  23. }

我们在结构体定义validator标签的tag,使用validator.New()创建一个验证器,这个验证器可以指定选项、添加自定义约束,然后在调用他的Struct()方法来验证各种结构对象的字段是否符合定义的约束。

上面的例子,我们在User结构体中,有三个字段:

  • Name:通过min和max来进行约束,Name的字符串长度为[6,10]之间。
  • Age:通过gte和lte对年轻的范围进行约束,age的大小大于1,小于10。
  • Sex:通过oneof对值进行约束,只能是所列举的值

所以user1会进行报错,错误信息如下:

Key: ‘User.Name’ Error:Field validation for ‘Name’ failed on the ‘min’ tag Key: ‘User.Age’ Error:Field validation for ‘Age’ failed on the ‘lte’ tag Key: ‘User.Sex’ Error:Field validation for ‘Sex’ failed on the ‘oneof’ tag

2 约束

(1) 字符串

excludesall:不包含参数中任意的 UNICODE 字符,例如excludesall=ab;
excludesrune:不包含参数表示的 rune 字符,excludesrune=asong;
startswith:以参数子串为前缀,例如startswith=hi;
endswith:以参数子串为后缀,例如endswith=bye。
contains=:包含参数子串,例如contains=email;
containsany:包含参数中任意的 UNICODE 字符,例如containsany=ab;
containsrune:包含参数表示的 rune 字符,例如`containsrune=asong;
excludes:不包含参数子串,例如excludes=email;

(2) 范围

范围约束的字段类型分为三种:

  • 对于数值,我们则可以约束其值
  • 对于切片、数组和map,我们则可以约束其长度
  • 对于字符串,我们则可以约束其长度

ne:不等于参数值,例如ne=5;
gt:大于参数值,例如gt=5;
gte:大于等于参数值,例如gte=50;
lt:小于参数值,例如lt=50;
lte:小于等于参数值,例如lte=50;
oneof:只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号包围,例如oneof=male female。
eq:等于参数值,注意与len不同。对于字符串,eq约束字符串本身的值,而len约束字符串长度。例如eq=10;
len:等于参数值,例如len=10;
max:小于等于参数值,例如max=10;
min:大于等于参数值,例如min=10

(3) 字段间

eqfield:定义字段间的相等约束,用于约束同一结构体中的字段。例如:eqfield=Password
eqcsfield:约束统一结构体中字段等于另一个字段(相对),确认密码时可以使用,例如:eqfiel=ConfirmPassword
nefield:用来约束两个字段是否相同,确认两种颜色是否一致时可以使用,例如:nefield=Color1
necsfield:约束两个字段是否相同(相对)

(4) 其它常用

unique:指定唯一性约束,不同类型处理不同:

  • 对于map,unique约束没有重复的值
  • 对于数组和切片,unique没有重复的值
  • 对于元素类型为结构体的碎片,unique约束结构体对象的某个字段不重复,使用unique=field指定字段名

email:使用email来限制字段必须是邮件形式,直接写eamil即可,无需加任何指定。
omitempty:字段未设置,则忽略
-:跳过该字段,不检验;
|:使用多个约束,只需要满足其中一个,例如rgb|rgba;
required:字段必须设置,不能为默认值;

3 自定义结构体校验

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "time"
  6. "github.com/gin-gonic/gin"
  7. "github.com/gin-gonic/gin/binding"
  8. "github.com/go-playground/validator/v10"
  9. )
  10. type Info struct {
  11. CreateTime time.Time `form:"create_time" binding:"required,timing" time_format:"2006-01-02"`
  12. UpdateTime time.Time `form:"update_time" binding:"required,timing" time_format:"2006-01-02"`
  13. }
  14. // 自定义验证规则断言
  15. func timing(fl validator.FieldLevel) bool {
  16. if date, ok := fl.Field().Interface().(time.Time); ok {
  17. today := time.Now()
  18. if today.After(date) {
  19. return false
  20. }
  21. }
  22. return true
  23. }
  24. func main() {
  25. route := gin.Default()
  26. // 注册验证
  27. if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  28. err := v.RegisterValidation("timing", timing)
  29. if err != nil {
  30. fmt.Println("success")
  31. }
  32. }
  33. route.GET("/time", func(c *gin.Context) {
  34. var b Info
  35. // 数据模型绑定查询字符串验证
  36. if err := c.ShouldBindWith(&b, binding.Query); err == nil {
  37. c.JSON(http.StatusOK, gin.H{"message": "time are valid!"})
  38. } else {
  39. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  40. }
  41. })
  42. route.Run(":8080")
  43. }

4 错误信息翻译

validator默认返回错误时都是英文的,validator库本身是支持国际化的,借助相应的语言包可以实现校验错误提示信息的自动翻译。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "github.com/gin-gonic/gin"
  7. "github.com/gin-gonic/gin/binding"
  8. "github.com/go-playground/locales/en"
  9. "github.com/go-playground/locales/zh"
  10. ut "github.com/go-playground/universal-translator"
  11. "github.com/go-playground/validator/v10"
  12. enTranslations "github.com/go-playground/validator/v10/translations/en"
  13. chTranslations "github.com/go-playground/validator/v10/translations/zh"
  14. )
  15. var trans ut.Translator
  16. // local 通常取决于 http 请求头的 'Accept-Language'
  17. func transInit(local string) (err error) {
  18. if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  19. zhT := zh.New() //chinese
  20. enT := en.New() //english
  21. uni := ut.New(enT, zhT, enT)
  22. var o bool
  23. trans, o = uni.GetTranslator(local)
  24. if !o {
  25. return fmt.Errorf("uni.GetTranslator(%s) failed", local)
  26. }
  27. //register translate
  28. // 注册翻译器
  29. switch local {
  30. case "en":
  31. err = enTranslations.RegisterDefaultTranslations(v, trans)
  32. case "zh":
  33. err = chTranslations.RegisterDefaultTranslations(v, trans)
  34. default:
  35. err = enTranslations.RegisterDefaultTranslations(v, trans)
  36. }
  37. return
  38. }
  39. return
  40. }
  41. type loginRequest struct {
  42. Username string `json:"username" binding:"required"`
  43. Password string `json:"password" binding:"required,max=16,min=6"`
  44. }
  45. func main() {
  46. if err := transInit("zh"); err != nil {
  47. fmt.Printf("init trans failed, err:%v\n", err)
  48. return
  49. }
  50. router := gin.Default()
  51. router.POST("/user/login", login)
  52. err := router.Run(":8888")
  53. if err != nil {
  54. log.Println("failed")
  55. }
  56. }
  57. func login(c *gin.Context) {
  58. var req loginRequest
  59. if err := c.ShouldBindJSON(&req); err != nil {
  60. // 获取validator.ValidationErrors类型的errors
  61. errs, ok := err.(validator.ValidationErrors)
  62. if !ok {
  63. // 非validator.ValidationErrors类型错误直接返回
  64. c.JSON(http.StatusOK, gin.H{
  65. "msg": err.Error(),
  66. })
  67. return
  68. }
  69. // validator.ValidationErrors类型错误则进行翻译
  70. c.JSON(http.StatusOK, gin.H{
  71. "msg": errs.Translate(trans),
  72. })
  73. return
  74. }
  75. //login 操作省略
  76. c.JSON(http.StatusOK, gin.H{
  77. "code": 0,
  78. "msg": "success",
  79. })
  80. }