In the daily development of Golang, it is sometimes necessary to validate each field of a struct to determine whether the value of the structure meets the conditions.

Consider the following profile structure:

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(a) profile {
	companies := []company{
		{
			Position: "frontend",
			Skills:   []string{"html"."css"},
		},
		{
			Position: "backend",
			Skills:   []string{"C"."Golang"},
		},
	}
	info := basicInfo{Name: "liang", Age: 24, Email: "[email protected]"}
	return profile{
		Info:      &info,
		Companies: companies,
	}
}
Copy the code

For values of type profile, there are the following restrictions:

  • Infofield
    • InfoDon’t is nil
    • NameIs limited to [1,20]
    • AgeThe value range of is [18,80].
    • EmailThe length is limited to [1,64] and conforms to the mailbox format
  • Companiesfield
    • PositionThe value can be frontend or Backend
    • ifPositionIt’s frontend, so the value of the element can only be HTML, CSS,javascript.
    • ifPositionBackend can be C,Cpp,Java, or Golang.
  • SkillsIs limited to [1,3]

Check structure parameters using the if/else, gin’s validator, and Checker methods.

Using an if/else

Use if/else to check if the struct argument is valid.

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 -], 21 {0} [a zA - Z0-9])? (? :\\.[a-zA-Z0-9](? : [a - zA - Z0-9 -], 21 {0} [a zA - Z0-9])?) * $")

   if! re.MatchString(pro.Info.Email){return false
   }
   for _, comp := range pro.Companies {
      if len(comp.Skills) > 3 {
         return false
      }
      ifcomp.Position ! ="frontend"&& comp.Position ! ="backend" {
         return false
      }
      if comp.Position == "frontend" {
         for _, skill := range comp.Skills {
            ifskill ! ="html"&& skill ! ="css"&& skill ! ="javascript" {
               return false}}}else if comp.Position == "backend" {
         for _, skill := range comp.Skills {
            ifskill ! ="C"&& skill ! ="Cpp"&& skill ! ="Java"&& skill ! ="Golang" {
               return false}}}}return true
}
Copy the code

As you can see, for the above validation rules, you may need to write large if/else statements. When the statements are too long, they are not readable and are strongly coupled to the structure.

Use go.pkg validatior

Go.pkg validator, which validates structures by adding tags to their fields.

The profile structure is modified to:

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"`
}
Copy the code

Check function changed to:

import 	"gopkg.in/go-playground/validator.v10"

func TestValidator(t *testing.T) {
	pro := getPassedProfile()
	validate := validator.New()
	err := validate.Struct(pro)
	iferr ! =nil {
		t.Errorf("%s", err.Error())
		return
	}
	for _, comp := range pro.Companies {
		ifcomp.Position ! ="frontend"&& comp.Position ! ="backend" {
			t.Error("failed")}if comp.Position == "frontend" {
			for _, skill := range comp.Skills {
				ifskill ! ="html"&& skill ! ="css"&& skill ! ="javascript" {
					t.Error("failed")}}}else if comp.Position == "backend" {
			for _, skill := range comp.Skills {
				ifskill ! ="C"&& skill ! ="Cpp"&& skill ! ="Java"&& skill ! ="Golang" {
					t.Error("failed")
				}
			}
		}
	}
	t.Log("passed")}Copy the code

Gopkg. in/go-playground/validator.v10 reduces some code, but the validation logic needs to be written on the tag of the structure, which increases code coupling. In addition, Validators do not support validation of enumerations.

Use the checker

The checker, made up of rules and checkers, adds rules externally to each field of a structure, reducing code coupling, and provides rules for composition, enumeration, and so on, making it easy to combine different rules freely.

func getProfileChecker(a) 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(""And []string{"html"."css"."javascript"}),
		),
	})
	backendRule := checker.NewAndRule([]checker.Rule{
		checker.NewEqRuleString("Position"."backend"),
		checker.NewSliceRule("Skills",
			checker.NewEnumRuleString(""And []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")

Copy the code

The TestProfileCheckerPassed function can be verified without adding additional code through the checker free collocation, reducing code coupling. The verification logic is all in the checker, so the verification logic is clearer.

Reference documentation

  1. checker

My official account: THE place to share lyP

My Zhihu column: zhuanlan.zhihu.com/c_127546654…

My blog: www.liangyaopei.com

Github Page: liangyaopei.github.io/