Golang结构体校验

1,763 阅读2分钟

在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不为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判断‘结构体参数是否合法。

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结构体要改造成:

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

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

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分享的地方

我的知乎专栏: zhuanlan.zhihu.com/c_127546654…

我的博客:www.liangyaopei.com

Github Page: liangyaopei.github.io/