4

Introduction

ozzo-validation is a very powerful and flexible data verification library. Unlike other data verification libraries based on struct tag, ozzo-validation believes that struct tag is more prone to errors during use. Because struct tag is essentially a string, it is completely based on string analysis and cannot use the language's static checking mechanism. It is easy to write errors unknowingly and not easily, and it is difficult to troubleshoot errors in actual code.

ozzo-validation advocates using codes to specify rules for verification. In fact ozzo is to assist the development of a Web application framework, including ORM library ozzo-dbx , routing library ozzo-routing , log database ozzo-log , configuration repository ozzo-config and the most famous, the most widely used data validation Library ozzo-validation . The author even came up with a template go-rest-api developing Web applications.

Quick to use

The code in this article uses Go Modules.

Create a directory and initialize:

$ mkdir ozzo-validation && cd ozzo-validation
$ go mod init github.com/darjun/go-daily-lib/ozzo-validation

Install the ozzo-validation library:

$ go get -u github.com/go-ozzo/ozzo-validation/v4

ozzo-validation is more intuitive to write:

package main

import (
  "fmt"

  "github.com/go-ozzo/ozzo-validation/v4/is"
  "github.com/go-ozzo/ozzo-validation/v4"
)

func main() {
  name := "darjun"

  err := validation.Validate(name,
    validation.Required,
    validation.Length(2, 10),
    is.URL)
  fmt.Println(err)
}

ozzo-validation uses the function Validate() to verify the basic type value. The first parameter passed in is the data to be verified, and then one or more verification rules are passed in with variable parameters. In the above example, a string is checked. We use code to express the rules:

  • validation.Required : Indicates that the value must be set, for a string, it cannot be empty;
  • validation.Length(2, 10) : Specify the range of length;
  • is.URL : A large number of auxiliary methods are built in the is is.URL must be in URL format.

Validate() function checks the data in sequence according to the incoming rules, until a certain rule fails to be checked, or all rules are checked successfully. If a rule returns a failure, skip the following rules and return an error directly. If the data passes all the rules, a nil is returned.

Run the above program output:

must be a valid URL

Because the string "darjun" is obviously not a valid URL. If the is.URL rule is removed, the run output nil .

Structure

Use ValidateStruct() function to check a structure object. We need to specify the verification rules for each field in the structure in turn:

type User struct {
  Name  string
  Age   int
  Email string
}

func validateUser(u *User) error {
  err := validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email))

  return err
}

ValidateStruct() accepts a structure pointer as the first parameter, and then specifies the rules of each field in turn. Field rules validation.Field() function, which accepts a pointer to a specific field, followed by one or more rules. Above we limit, the length of the name is between [2, 10], the age is between [1, 200] (let’s assume that humans can live up to 200 years now), the length of the e-mail address is between [10, 50], and use is.Email restricts it must be a legal email address. At the same time, these 3 fields are required ( validation.Required ).

Then we construct a legal User object and an illegal User object, and verify them separately:

func main() {
  u1 := &User {
    Name: "darjun",
    Age: 18,
    Email: "darjun@126.com",
  }
  fmt.Println("user1:", validateUser(u1))

  u2 := &User {
    Name: "lidajun12345",
    Age: 201,
    Email: "lidajun's email",
  }
  fmt.Println("user2:", validateUser(u2))
}

Program output:

user1: <nil>
user2: Age: must be no greater than 200; Email: must be a valid email address; Name: the length must be between 2 and 10.

For the structure, validation checks the incoming rules for each field in turn. For a certain field, if a rule fails the check, the following rule is skipped, continues to check the next field . If a field fails to verify, the error information about the field will be included in the result, as in the example above.

Map

Sometimes the data is stored in a map instead of a structure. At this time, you can use validation.Map() specify the rules for verifying map . In the validation.Map() rules, you need to use validation.Key() specify one or more rules corresponding to each key in turn. Finally, pass the map type data and the validation.Map() rule to the validation.Validate() function to check:

func validateUser(u map[string]interface{}) error {
  err := validation.Validate(u, validation.Map(
    validation.Key("name", validation.Required, validation.Length(2, 10)),
    validation.Key("age", validation.Required, validation.Min(1), validation.Max(200)),
    validation.Key("email", validation.Required, validation.Length(10, 50), is.Email),
  ))

  return err
}

func main() {
  u1 := map[string]interface{} {
    "name": "darjun",
    "age": 18,
    "email": "darjun@126.com",
  }
  fmt.Println("user1:", validateUser(u1))

  u2 := map[string]interface{} {
    "name": "lidajun12345",
    "age": 201,
    "email": "lidajun's email",
  }
  fmt.Println("user2:", validateUser(u2))
}

We modified the above example and used map[string]interface{} store User information. map is similar to the structure, and it is verified sequentially according to the order of the keys specified in validation.Map() If a key fails to verify, record the error message. Finally, the error information of all keys is summarized and returned. Run the program:

user1: <nil>
user2: age: must be no greater than 200; email: must be a valid email address; name: the length must be between 2 and 10.

Verifiable type

ozzo-validation library provides an interface Validatable :

type Validatable interface {
  // Validate validates the data and returns an error if validation fails.
  Validate() error
}

All types that implement the Validatable interface are verifiable types. validation.Validate() function verifies a certain type of data, it first verifies all the rules passed into the function. If these rules are passed, the Validate() function determines whether the type has implemented the Validatbale interface. If it is achieved, call its Validate() method for verification. We let the User type in the Validatable interface:

type User struct {
  Name   string
  Age    int
  Gender string
  Email  string
}

func (u *User) Validate() error {
  err := validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.Gender, validation.Required, validation.In("male", "female")),
    validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email))

  return err
}

Since User implements the Validatable interface, we can directly call the Validate() function to verify:

func main() {
  u1 := &User{
    Name:   "darjun",
    Age:    18,
    Gender: "male",
    Email:  "darjun@126.com",
  }
  fmt.Println("user1:", validation.Validate(u1, validation.NotNil))

  u2 := &User{
    Name:  "lidajun12345",
    Age:   201,
    Email: "lidajun's email",
  }
  fmt.Println("user2:", validation.Validate(u2, validation.NotNil))
}

After passing the NotNil verification, the Validate() function will also call the User.Validate() method for verification.

It should be noted that Validate() method of the type that Validatable interface, you cannot directly call the validation.Validate() function for the value of the type, which will lead to infinite recursion:

type UserName string

func (n UserName) Validate() error {
  return validation.Validate(n,
    validation.Required, validation.Length(2, 10))
}

func main() {
  var n1, n2 UserName = "dj", "lidajun12345"

  fmt.Println("username1:", validation.Validate(n1))
  fmt.Println("username2:", validation.Validate(n2))
}

We defined a new type UserName based on string , stipulating that UserName not empty and the length is in the range of [2, 10]. However, the above Validate() in the method UserName type variable n passed as a function validation.Validate() . The internal inspection of this function finds that UserName implements the Validatable interface, and then calls its Validate() method, resulting in infinite recursion.

We only need to simply convert n to string type:

func (n UserName) Validate() error {
  return validation.Validate(string(n),
    validation.Required, validation.Length(2, 10))
}

A collection of verifiable types

Validate() function verifies a collection (slice/array/map, etc.) whose elements are of a Validate() type (that is, implements the Validatable interface), it will call the 060d4a5075f584 method of its elements in turn, and finally the verification returns a validation.Errors type. This is actually a map[string]error type. The key is the key of the element (the index is for slices and arrays, and the key for map ), and the value is an error value. example:

func main() {
  u1 := &User{
    Name:   "darjun",
    Age:    18,
    Gender: "male",
    Email:  "darjun@126.com",
  }
  u2 := &User{
    Name:  "lidajun12345",
    Age:   201,
    Email: "lidajun's email",
  }

  userSlice := []*User{u1, u2}
  userMap := map[string]*User{
    "user1": u1,
    "user2": u2,
  }

  fmt.Println("user slice:", validation.Validate(userSlice))
  fmt.Println("user map:", validation.Validate(userMap))
}

userSlice sections parity error in the second element of the key will result 1 return (index) of, userMap button user2 parity error will result in the key user2 Returns. operation result:

user slice: 1: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).
user map: user2: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).

If we need to satisfy certain rules for every element in the set, we can use the validation.Each() function. For example, our User object has multiple mailboxes, and the format of each mailbox address is required to be legal:

type User struct {
  Name   string
  Age    int
  Emails []string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Emails, validation.Each(is.Email)))
}

func main() {
  u := &User{
    Name: "dj",
    Age:  18,
    Emails: []string{
      "darjun@126.com",
      "don't know",
    },
  }
  fmt.Println(validation.Validate(u))
}

The error message will indicate which location data is invalid:

Emails: (1: must be a valid email address.).

Conditional rule

We can set rules for another field based on the value of one field. For example, our User object has two fields: the boolean Student indicates whether it is still a student, and the string School indicates the school. When Student is true , the field School must exist and the length is in the range of [10, 20]:

type User struct {
  Name    string
  Age     int
  Student bool
  School  string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.School, validation.When(u.Student, validation.Required, validation.Length(10, 20))))
}

func main() {
  u1 := &User{
    Name:    "dj",
    Age:     18,
    Student: true,
  }

  u2 := &User{
    Name: "lidajun",
    Age:  31,
  }

  fmt.Println("user1:", validation.Validate(u1))
  fmt.Println("user2:", validation.Validate(u2))
}

We use the validation.When() function, which accepts a Boolean value as the first parameter, and one or more rules as the variable parameters. Only when the first parameter is true , will the following rule check be performed.

u1 Because the field Student is true , the School field cannot be empty. u2 because Student=false , School fields are optional. run:

user1: School: cannot be blank.
user2: <nil>

When checking the registered user information, we make sure that the user must set the email or mobile phone number, and the conditional rules can also be used:

type User struct {
  Email string
  Phone string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Email, validation.When(u.Phone == "", validation.Required.Error("Either email or phone is required."), is.Email)),
    validation.Field(&u.Phone, validation.When(u.Email == "", validation.Required.Error("Either email or phone is required."), is.Alphanumeric)))
}

func main() {
  u1 := &User{}

  u2 := &User{
    Email: "darjun@126.com",
  }

  u3 := &User{
    Phone: "17301251652",
  }

  u4 := &User{
    Email: "darjun@126.com",
    Phone: "17301251652",
  }

  fmt.Println("user1:", validation.Validate(u1))
  fmt.Println("user2:", validation.Validate(u2))
  fmt.Println("user3:", validation.Validate(u3))
  fmt.Println("user4:", validation.Validate(u4))
}

If the Phone field is empty, Email must be set. Conversely, if the Email field is empty, Phone must be set. All rules can call the Error() method to set custom error messages. Run output:

user1: Email: Either email or phone is required.; Phone: Either email or phone is required..
user2: <nil>
user3: <nil>
user4: <nil>

Custom rule

In addition to the rules provided by the library, we can also define our own rules. The rule is implemented as a function of the following type:

func Validate(value interface{}) error

Below we implement a function to check whether the IP address is legal. Here we introduce a library commonregex . This library contains most of the commonly used regular expressions. I also wrote an article before to introduce the use of this library, daily library. you are interested, you can check it out.

func checkIP(value interface{}) error {
  ip, ok := value.(string)
  if !ok {
    return errors.New("ip must be string")
  }

  ipList := commonregex.IPs(ip)
  if len(ipList) != 1 || ipList[0] != ip {
    return errors.New("invalid ip format")
  }

  return nil
}

Then define a network address structure and verification method, and use a custom verification function validation.By()

type Addr struct {
  IP   string
  Port int
}

func (a *Addr) Validate() error {
  return validation.ValidateStruct(a,
    validation.Field(&a.IP, validation.Required, validation.By(checkIP)),
    validation.Field(&a.Port, validation.Min(1024), validation.Max(65536)))
}

verification:

func main() {
  a1 := &Addr{
    IP:   "127.0.0.1",
    Port: 6666,
  }

  a2 := &Addr{
    IP:   "xxx.yyy.zzz.hhh",
    Port: 7777,
  }

  fmt.Println("addr1:", validation.Validate(a1))
  fmt.Println("addr2:", validation.Validate(a2))
}

run:

addr1: <nil>
addr2: IP: invalid ip format.

Rule set

It is a bit inconvenient to specify the rules one by one each time. At this time, we can group the commonly used verification rules into a rule group, and use this group directly when needed. For example, the legal user name agreed in our project must be ASCII letters and numbers, with a length of 10-20, and the user name must not be empty. The rule group is nothing special, it is just a slice of the rule:

var NameRule = []validation.Rule{
  validation.Required,
  is.Alphanumeric,
  validation.Length(10, 20),
}

func main() {
  name1 := "lidajun12345"
  name2 := "lidajun@!#$%"
  name3 := "short"
  name4 := "looooooooooooooooooong"

  fmt.Println("name1:", validation.Validate(name1, NameRule...))
  fmt.Println("name2:", validation.Validate(name2, NameRule...))
  fmt.Println("name3:", validation.Validate(name3, NameRule...))
  fmt.Println("name4:", validation.Validate(name4, NameRule...))
}

run:

name1: <nil>
name2: must contain English letters and digits only
name3: the length must be between 10 and 20
name4: the length must be between 10 and 20

to sum up

ozzo-validation promote a code specifies rules instead of error-prone struct tag , and provides a number of built-in rule. The code written with ozzo-validation is clear, easy to read, and friendly to the compiler (many errors are exposed at compile time). This article introduces the ozzo-validation library. The core is two functions Validate() and ValidateStruct() . The former is used to verify basic types or verifiable types, and the latter is used to verify structures. In the actual encoding process, the structure will generally implement the Validatbale interface to change it into a Validate() type, and then call the 060d4a5075fa94 function to verify.

ozzo-validation can also check the collection, you can customize the check rules, and you can define a general check group. In addition, ozzo-validation has many advanced features, such as custom errors, context.Context , using regular expressions to define rules, etc. If you are interested, you can explore it yourself.

If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

reference

  1. ozzo-validation GitHub:github.com/go-ozzo/ozzo-validation
  2. go-rest-api GitHub:github.com/qiangxue/go-rest-api
  3. Go Daily Library: 160d4a5075fb65 https://darjun.github.io/2020/09/05/godailylib/commonregex/
  4. Go a library GitHub every day: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望356 粉丝