Random walk to my blog

my blog for sharing my knowledge,experience and viewpoint

0%

Golang结构体校验

在Golang的日常开发中,有时候需要对struct的每个字段(field)进行校验,从而判断结构体的值是否符合条件。

考虑下面的profile结构体:

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
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不为nil
    • Name的长度限制为[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判断‘结构体参数是否合法。

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
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.pkgvalidator,它是通过在结构体的字段添加标签(tag),来校验结构体。

profile结构体要改造成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"`
}

校验函数改为:

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
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

本文介绍的checkerRuleChecker组成,在外部对结构体的每一个字段添加规则,降低代码耦合性,并且提供组合规则,枚举等规则,可以轻松实现不同规则的自由组合。

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
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里面,校验逻辑更为清晰。

参考文档

  1. checker

我的公众号:lyp分享的地方

我的知乎专栏: https://zhuanlan.zhihu.com/c_1275466546035740672

我的博客:www.liangyaopei.com

Github Page: https://liangyaopei.github.io/