Golang使用Gin
框架的开发中,服务器在绑定HTTP请求的参数后,需要对参数进行校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Req struct { Info typeInfo Email string } type typeStr string type typeInfo struct { Type typeStr Range []string Unit string Granularity string } func BindParams (ctx *gin.Context) { req := Req{} _ = ctx.BindJSON(&req) }
参数校验一般有3种方式:
使用if/else或者switch的原生的校验方法。
使用gin
自带的结构体标签来校验。
使用checker 进行声明式的参数校验。
原生的校验方法 可以看到,原生的if/else,switch的的校验方法比较繁琐,不容易阅读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func (r Req) validate () bool { emailRegexString := "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" regexObject := regexp.MustCompile(emailRegexString) if !regexObject.MatchString(r.Email) { return false } switch r.Info.Type { case "range" : if len (r.Info.Range) != 2 { return false } for _, value := range r.Info.Range { if _, err := time.Parse("2006-01-02" , value); err != nil { return false } } case "last" : if len (r.Info.Range) != 1 { return false } valInt, err := strconv.Atoi(r.Info.Range[0 ]) if err != nil { return false } if valInt <= 0 { return false } if r.Info.Granularity != "hour" && r.Info.Granularity != "day" && r.Info.Granularity != "week" && r.Info.Granularity != "month" && r.Info.Granularity != "year" && r.Info.Granularity != "decade" { return false } default : return false } return true }
结构体标签校验 在结构体定义时,在字段加上标签:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Req struct { Info typeInfo Email string `binding:"required,email"` } type typeStr string type typeInfo struct { Type typeStr Range []string `binding:"required,min=1,max=2"` Granularity string }
结构体标签校验的缺点有:
支持的方法不完整,例如Granularity
的枚举校验并没有对应的标签。
标签与结构体强耦合。如果同个结构体,想有多个校验方法,目前还不支持。例如,Range
字段在Type=range
的长度是2,Type=last
的长度是1.
难以阅读,容易出错。将较为复杂的校验规则写在标签上,不利于代码可读性。而且,如果一个int字段绑定了字符类型的校验标签,就会panic
。
Checker checker 通过在结构体外部定义校验规则,降低耦合度。 并且,有大量的规则方便校验。
checker
中一个重要的方法是fetchFiled
,它用来在结构体中获取字段的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func fetchField (param interface {}, filedExpr string ) (interface {}, reflect.Kind) { pValue := reflect.ValueOf(param) if filedExpr == "" { return param, pValue.Kind() } exprs := strings.Split(filedExpr, "." ) for i := 0 ; i < len (exprs); i++ { expr := exprs[i] if pValue.Kind() == reflect.Ptr { pValue = pValue.Elem() } if !pValue.IsValid() || pValue.Kind() != reflect.Struct { return nil , reflect.Invalid } pValue = pValue.FieldByName(expr) } if pValue.Kind() == reflect.Ptr { if pValue.IsNil() { return nil , reflect.Ptr } pValue = pValue.Elem() } if !pValue.IsValid() { return nil , reflect.Invalid } return pValue.Interface(), pValue.Kind() }
fetchField
的参数fieldExpr
有三种情况:
fieldExpr
为空字符串,这时会直接校验值。
fieldExpr
为单个字段,这时会先取字段的值,再校验。
fieldExpr
为点(.)分割的字段,先按照.
的层级关系取值,再校验
按字段取值时,如果字段是指针,就取指针的值校验;如果是空指针,则视为没有通过校验。
上述的校验规则使用checker 改写为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 rule := And( Email("Email" ), Field("Info" , Or( And( EqStr("Type" , "range" ), Length("Range" , 2 , 2 ), Array("Range" , isDatetime("" , "2006-01-02" )), ), And( EqStr("Type" , "last" ), InStr("Granularity" , "day" , "week" , "month" ), Number("Unit" ), ), ), ), ) itemChecker := NewChecker() itemChecker.Add(rule, "wrong item" )
相比其他两种方法,Rule
的定义,更为简洁,清晰,强大,耦合度低。
Bonus checker 中fieldExpr
的定义, 使得它可以用来校验链表的值以及链表的长度,这是使用标签的校验方法无法做到的。
1 2 3 4 5 6 7 type list struct { Name *string Next *list } nameRule := Length("Next.Next.Name" , 1 , 20 )
自定义校验 日常开发中,函数库现有的校验规则不能满足需求,这时候需要自定义校验规则。下面比较Gin
和Checker
实现判断字段是否为nil的校验规则。
Gin的自定义校验 使用Gin
的校验需要对Gin
的校验Engine有一定了解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type customizedStruct struct { NilPtr *int } func nilPtr (fl validator.FieldLevel) bool { if fl.Field().Interface()==nil { return true ; } return false ; } func main () { route := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { err := v.RegisterValidation("nilPtr" , nilPtr) if err != nil { fmt.Println("success" ) } } }
Checker的自定义校验 用户通过实现Rule
接口,来实现自定义的校验规则。下面的customizedRule
, 用来校验fieldExpr
是否为空指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type customizedRule struct { fieldExpr string name string } func (r customizedRule) Check (param interface {}) (bool , string ) { exprValue, _ := fetchField(param, r.fieldExpr) if exprValue != nil { return false ,fmt.Sprintf("[%s]:'%s' is not nil" , r.name, r.fieldExpr) } return true , "" } func main () { ch := NewChecker() customRule := customizedRule{} ch.Add(customRule, "invalid ptr" ) }
自定义错误提示 日常开发中,除了校验结构体外,还需要根据校验结果,给出不同的错误提示。
原生的校验方法 将上述Req
的validate
方法改为,需要返回特定的错误提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 type Req struct { Info typeInfo Email string `binding:"required,email"` } type typeStr string type typeInfo struct { Type typeStr Range []string `binding:"required,min=1,max=2"` Granularity string } func (r Req) validate () (bool ,string ) { emailRegexString := "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" regexObject := regexp.MustCompile(emailRegexString) if !regexObject.MatchString(r.Email) { return false } switch r.Info.Type { case "range" : if len (r.Info.Range) != 2 { return false , "错误的range长度" } for _, value := range r.Info.Range { if _, err := time.Parse("2006-01-02" , value); err != nil { return false , "错误的range值" } } case "last" : if len (r.Info.Range) != 1 { return false ,"错误的range长度" } valInt, err := strconv.Atoi(r.Info.Range[0 ]) if err != nil { return false , "错误的range值" } if valInt <= 0 { return false } if r.Info.Granularity != "hour" && r.Info.Granularity != "day" && r.Info.Granularity != "week" && r.Info.Granularity != "month" && r.Info.Granularity != "year" && r.Info.Granularity != "decade" { return false , "错误的 granalrity" } default : return false , "错误的 type" } return true ,"" }
Gin的自定义错误提示 Gin
的错误提示一般是用于开发者调试的,不能直接返回。
1 2 3 4 5 6 7 8 9 10 11 type LoginRequest struct { Mobile string `form:"mobile" json:"mobile" binding:"required"` Code string `form:"code" json:"code" binding:"required"` } router.POST("/loginJSON" , func (c *gin.Context) { var json Login if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } })
在ShouldBindJSON
返回的错误信息, 一般难以阅读,并且不能直接返回给前端。
1 2 3 { "message" : "Key: 'LoginRequest.Mobile' Error:Field validation for 'Mobile' failed on the 'required' tag\nKey: 'LoginRequest.Code' Error:Field validation for 'Code' failed on the 'required' tag" }
Gin的自定义错误提示比较复杂,可以参考gin框架自定义验证错误提示信息 。
Checker的自定义错误提示 Checker
是一个接口
Add(rule Rule, prompt string)
: 添加规则,和没有通过规则是的错误提示。
Check(param interface{}) (bool, string, string)
: 校验参数,依次返回是否通过校验,错误提示,错误日志。错误日志包含哪个字段没有通过哪个规则的信息。
想要自定义错误提示,只需要添加规则的时候,附上没有通过校验规则的错误提示即可。
1 itemChecker.Add(rule, "wrong item" )
参考文档
checker