Golang使用Gin
框架的开发中,服务器在绑定HTTP请求的参数后,需要对参数进行校验:
// Req.Email需要符合电子邮箱的格式
type Req struct {
Info typeInfo
Email string
}
type typeStr string
// Req.Info.Type = "range",typeInfo.Type的长度为2,元素都是格式符合"2006-01-02"
// Req.Info.Type = "last",typeInfo.Type的长度为1,元素是正整数,Granularity只能是day/week/month之一
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的的校验方法比较繁琐,不容易阅读。
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
}
结构体标签校验
在结构体定义时,在字段加上标签:
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
,它用来在结构体中获取字段的值。
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)
}
// last field is pointer
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改写为:
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
的定义,
使得它可以用来校验链表的值以及链表的长度,这是使用标签的校验方法无法做到的。
type list struct {
Name *string
Next *list
}
// 校验第2个节点的Name的长度在[1,20]
nameRule := Length("Next.Next.Name", 1, 20)
参考文档: