在Golang的日常开发中,有时候需要对struct
的每个字段(field)进行校验,从而判断结构体的值是否符合条件。
考虑下面的profile
结构体:
type profile struct {
// Info is pointer filed
Info *basicInfo
Companies []company
}
type basicInfo struct {
// 1 <= len <= 20
Name string
// 18 <= age <= 80
Age int
// 1<= len <= 64
Email string
}
type company struct {
// frontend,backend
Position string
// frontend: html,css,javascript
// backend: C,Cpp,Java,Golang
// SkillStack 'length is between [1,3]
Skills []string
}
func getPassedProfile() profile {
companies := []company{
{
Position: "frontend",
Skills: []string{"html", "css"},
},
{
Position: "backend",
Skills: []string{"C", "Golang"},
},
}
info := basicInfo{Name: "liang", Age: 24, Email: "yaopei.liang@foxmail.com"}
return profile{
Info: &info,
Companies: companies,
}
}
对于profile
类型的值,有下面的限制:
Info
字段
Info
不为nilName
的长度限制为[1,20]Age
的取值范围是[18,80]Email
的长度限制为[1,64], 并且符合邮箱的格式
Companies
字段
Position
只能是frontend或者backend- 如果
Position
是frontend, 里面的元素取值只能是 html,css,javascript. - 如果
Position
是backend, 里面的元素取值只能是 C,Cpp,Java,Golang. -Skills
的长度限制为[1,3]
下面分别讲述使用if/else
, gin的校验器,和checker,三个方法对结构体参数进行校验。
使用if/else
使用if/else
判断‘结构体参数是否合法。
func isValidProfile(pro profile) bool {
if pro.Info == nil {
return false
}
if len(pro.Info.Name) > 20 {
return false
}
if pro.Info.Age < 18 && pro.Info.Age > 80 {
return false
}
if len(pro.Info.Email) > 64 {
return false
}
re := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
if !re.MatchString(pro.Info.Email){
return false
}
for _, comp := range pro.Companies {
if len(comp.Skills) > 3 {
return false
}
if comp.Position != "frontend" && comp.Position != "backend" {
return false
}
if comp.Position == "frontend" {
for _, skill := range comp.Skills {
if skill != "html" && skill != "css" && skill != "javascript" {
return false
}
}
} else if comp.Position == "backend" {
for _, skill := range comp.Skills {
if skill != "C" && skill != "Cpp" && skill != "Java" && skill != "Golang" {
return false
}
}
}
}
return true
}
可以看到,对于上述的校验规则,可能需要写大段的if/else
判断语句,当语句太长时,不适合阅读,并且与结构体强耦合。
使用go.pkg的validatior
go.pkg
的validator,它是通过在结构体的字段添加标签(tag),来校验结构体。
profile
结构体要改造成:
type profile struct {
Info *basicInfo
Companies []company `validate:"dive,min=1,max=3"`
}
type basicInfo struct {
Name string `validate:"min=1,max=20"`
Age int `validate:"min=18,max=80"`
Email string `validate:"min=1,max=64,email"`
}
type company struct {
// frontend,backend
Position string
// frontend: html,css,javascript
// backend: C,Cpp,Java,Golang
Skills []string `validate:"min=1,max=3"`
}
校验函数改为:
import "gopkg.in/go-playground/validator.v10"
func TestValidator(t *testing.T) {
pro := getPassedProfile()
validate := validator.New()
err := validate.Struct(pro)
if err != nil {
t.Errorf("%s", err.Error())
return
}
for _, comp := range pro.Companies {
if comp.Position != "frontend" && comp.Position != "backend" {
t.Error("failed")
}
if comp.Position == "frontend" {
for _, skill := range comp.Skills {
if skill != "html" && skill != "css" && skill != "javascript" {
t.Error("failed")
}
}
} else if comp.Position == "backend" {
for _, skill := range comp.Skills {
if skill != "C" && skill != "Cpp" && skill != "Java" && skill != "Golang" {
t.Error("failed")
}
}
}
}
t.Log("passed")
}
可以看到,gopkg.in/go-playground/validator.v10
虽然减少了部分代码,但是校验逻辑需要写在结构体的的标签上面,增加了代码耦合。另外,validator
还不支持枚举的校验。
使用checker
本文介绍的checker由Rule
和Checker
组成,在外部对结构体的每一个字段添加规则,降低代码耦合性,并且提供组合规则,枚举等规则,可以轻松实现不同规则的自由组合。
func getProfileChecker() checker.Checker {
profileChecker := checker.NewChecker()
infoNameRule := checker.NewLengthRule("Info.Name", 1, 20)
profileChecker.Add(infoNameRule, "invalid info name")
infoAgeRule := checker.NewRangeRuleInt("Info.Age", 18, 80)
profileChecker.Add(infoAgeRule, "invalid info age")
infoEmailRule := checker.NewAndRule([]checker.Rule{
checker.NewLengthRule("Info.Email", 1, 64),
checker.NewEmailRule("Info.Email"),
})
profileChecker.Add(infoEmailRule, "invalid info email")
companyLenRule := checker.NewLengthRule("Companies", 1, 3)
profileChecker.Add(companyLenRule, "invalid companies len")
frontendRule := checker.NewAndRule([]checker.Rule{
checker.NewEqRuleString("Position", "frontend"),
checker.NewSliceRule("Skills",
checker.NewEnumRuleString("", []string{"html", "css", "javascript"}),
),
})
backendRule := checker.NewAndRule([]checker.Rule{
checker.NewEqRuleString("Position", "backend"),
checker.NewSliceRule("Skills",
checker.NewEnumRuleString("", []string{"C", "CPP", "Java", "Golang"}),
),
})
companiesSliceRule := checker.NewSliceRule("Companies",
checker.NewAndRule([]checker.Rule{
checker.NewLengthRule("Skills", 1, 3),
checker.NewOrRule([]checker.Rule{
frontendRule, backendRule,
}),
}))
profileChecker.Add(companiesSliceRule, "invalid skill item")
return profileChecker
}
func TestProfileCheckerPassed(t *testing.T) {
profile := getPassedProfile()
profileChecker := getProfileChecker()
isValid, prompt, errMsg := profileChecker.Check(profile)
if !isValid {
t.Logf("prompt:%s", prompt)
t.Logf("errMsg:%s", errMsg)
return
}
t.Log("pass check")
通过的checker
的自由搭配,TestProfileCheckerPassed函数无需添加额外的代码,即可完成校验,降低了代码耦合性。校验的逻辑都在checker
里面,校验逻辑更为清晰。
参考文档
- checker
我的公众号:lyp分享的地方
我的知乎专栏: https:// zhuanlan.zhihu.com/c_12 75466546035740672
我的博客: http://www. liangyaopei.com
Github Page: https:// liangyaopei.github.io/