Validator is a very extensive framework component used in our usual business. Many web frameworks and microservice frameworks are integrated. It is usually used to check some request parameters to avoid writing duplicate check logic. In the next article, we will take a look at how to implement a validator.

First Experience

Practice is the primary productive force. I will provide a scenario first. Now we have an interface for filling in user information, which needs to be saved and stored in the database. What should we do?

First, we first define a structure to specify several parameters of user information:

type ValidateStruct struct {
    Name     string `json:"name"`
    Address string `json:"address"`
    Email   string `json:"email"`
}

The data sent by the user needs to be verified before it can be stored. For example, the Name is required, the Email is legal, etc., then how do we implement it? It can be like this:


func validateEmail(email string) error {
    //do something
    return nil
}
func validateV1(req ValidateStruct) error{
    if len(req.Name) > 0 {
        if len(req.Address) > 0 {
            if len(req.Email) > 0 {
                if err := validateEmail(req.Email); err != nil {
                    return err
                }
            }else {
                return errors.New("Email is required")
            }
        } else {
            return errors.New("Address is required")
        }
    } else {
        return errors.New("Name is required")
    }

    return nil
}

It can also be like this:

func validateV2(req ValidateStruct) error{
    if len(req.Name) < 0 {
        return errors.New("Name is required")
    }

    if len(req.Address) < 0 {
        return errors.New("Name is required")
    }

    if len(req.Email) < 0 || validateEmail(req.Email) != nil {
        return errors.New("Name is required")
    }

    return nil
}

It can be used, but it can be used. Just imagine, if we want to add 100 interfaces now, and each interface has different request parameters, don't we have to write this logic 100 times? That is impossible! Let's think of a way.

Advanced

We will find that although the parameter names are different, the verification logic can be the same. For example, if the parameter is greater than 0, less than 0, and not equal to this, the commonality can be found, so can we extract the general logic? Let's first look at our general logic. This method can help us realize the verification of int and string parameters. Because it is only for demonstration use, it is just a simple implementation to express the feasibility of this method.

func validateEmail(input string) bool {
    if pass, _ := regexp.MatchString(
        `^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, input,
    ); pass {
        return true
    }
    return false
}

//通用的校验逻辑,采用反射实现
func validate(v interface{}) (bool, string) {
    vt := reflect.TypeOf(v)
    vv := reflect.ValueOf(v)
    errmsg := "success"
    validateResult := true

    for i := 0; i < vt.NumField(); i++ {
        if errmsg != "success" {
            return validateResult, errmsg
        }

        fieldValue := vv.Field(i)
        tagContend := vt.Field(i).Tag.Get("validate")
        
        k := fieldValue.Kind()
        switch k {
        case reflect.Int64:
            val := fieldValue.Int()
            tagValStr := strings.Split(tagContend, "=")
            if tagValStr[0] != "eq" {
                errmsg = "validate int failed, tag is: " + tagValStr[0]
                validateResult = false
            }
            tagVal, _ := strconv.ParseInt(tagValStr[1], 10, 64)
            if val != tagVal {
                errmsg = "validate int failed, tag is: "+ strconv.FormatInt(
                    tagVal, 10,
                )
                validateResult = false
            }
        case reflect.String:
            valStr := fieldValue.String()
            tagValStr := strings.Split(tagContend, ";")
            for _, val := range tagValStr {
                if val == "email" {
                    nestedResult := validateEmail(valStr)
                    if nestedResult == false {
                        errmsg = "validate mail failed, field val is: "+ val
                        validateResult = false
                    }
                }

                tagValChildStr := strings.Split(val, "=")
                if tagValChildStr[0] == "gt" {
                    length, _ := strconv.ParseInt(tagValChildStr[1], 10, 64)
                    if len(valStr) <  int(length) {
                        errmsg = "validate int failed, tag is: "+ strconv.FormatInt(
                            length, 10,
                        )
                        validateResult = false
                    }
                }

            }
        case reflect.Struct:
            // 如果有内嵌的 struct,那么深度优先遍历
            // 就是一个递归过程
            valInter := fieldValue.Interface()
            nestedResult, msg := validate(valInter)
            if nestedResult == false {
                validateResult = false
                errmsg = msg
            }
        }
    }

    return validateResult, errmsg
}

Next, let's run it:

//定义我们需要的结构体
type ValidateStructV3 struct {
    // 字符串的 gt=0 表示长度必须 > 0,gt = greater than
    Name     string `json:"name" validate:"gt=0"`
    Address string `json:"address" validate:"gt=0"`
    Email   string `json:"email" validate:"email;gt=3"`
    Age     int64  `json:"age" validate:"eq=0"`
}


func ValidateV3(req ValidateStructV3) string {
    ret, err := validate(req)
    if !ret {
        println(ret, err)
        return err
    }

    return ""
}

//实现这个结构体
req := demos.ValidateStructV3{
        Name:    "nosay",
        Address: "beijing",
        Email:   "nosay@qq.com",
        Age: 3,
    }
resp := demos.ValidateV3(req)

//输出:validate int failed, tag is: 0

In this way, there is no need to write a duplicate validate() function before each request enters the business logic, and we can also integrate it into the framework.

Principle introduction

Just like our implementation of validator above, its principle is the structure shown in the figure below. If it is a judicious type, it will do the corresponding action through the tag. If it is a struct, it will recurse and continue to traverse.
image.png

The struct is our request body (that is, the parent node), and the child node corresponds to each of our elements. Its type is int64, string, struct or other types. We use the type to perform the corresponding behavior (that is, the int type of eq =0, gt=0 of string type, etc.).

For example, we run our validator in the following way:

type ValidateStructV3 struct {
    // 字符串的 gt=0 表示长度必须 > 0,gt = greater than
    Name     string `json:"name" validate:"gt=0"`
    Address string `json:"address" validate:"gt=0"`
    Email   EmailV4
    Age     int64  `json:"age" validate:"eq=0"`
}

type EmailV4 struct {
    // 字符串的 gt=0 表示长度必须 > 0,gt = greater than
    Email   string `json:"email" validate:"email;gt=3"`
}

req := demos.ValidateStructV3{
        Name:    "nosay",
        Address: "beijing",
        Email:   demos.EmailV4{
            Email: "nosayqq.com",
        },
        Age: 0,
    }

    resp := demos.ValidateV3(req)

At this time, its execution process looks like this:
image.png

Expand

In fact, we have already covered the basic principles here, but the real implementation is definitely not that simple. Here I recommend a special validator library ( https://github.com/go-playground/validator). Interested Readers can read it~

Follow us

Readers who are interested in this series of articles are welcome to subscribe to our official account, and pay attention to the blogger not to get lost next time~
image.png


NoSay
449 声望544 粉丝