【深入理解Go】從0到1實現一個validator

NoSay發表於2021-10-03

validator是我們平時業務中用的非常廣泛的框架元件,很多web框架、微服務框架都有整合。通常用來做一些請求引數的校驗以避免寫出重複的檢驗邏輯。接下來的文章中,我們就去看看如何去實現一個validator。

初體驗

實踐是第一生產力,我先提供一個場景,現在我們有一個介面,作用是填寫使用者資訊,需要我們儲存入庫。我們該怎麼做呢?

首先,我們先定義一個結構體,規定使用者資訊的幾個引數:

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

使用者傳進來資料,我們需要校驗才能入庫,例如Name是必填的,Email是合法的這些等等,那我們要怎麼去實現它?可以是這樣:


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
}

也可以是這樣:

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
}

可以用倒是可以用了,試想一下,如果現在我們要增加100個介面,每個介面有不同的請求引數,那這樣的邏輯我們豈不是要寫100遍?那是不可能的!我們再想想辦法。

進階

我們會發現引數名雖然不同,但是校驗邏輯是可以相同的,例如引數大於0,小於0,不等於這種,共性可以找到,那我們是不是就可以抽出通用的邏輯來了呢?先來看我們的通用邏輯,這個方法可以幫我們實現int,string引數的校驗,因為只是做演示使用,所以只是簡單的進行實現,以此來表達這種方式的可行性。

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
}

接下來我們來跑一下:

//定義我們需要的結構體
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

這樣就不需要在每個請求進入業務邏輯之前都寫重複的validate()函式了,我們同樣可以整合在框架裡。

原理介紹

正如我們上文validator的實現一樣,他的原理就是下圖這個結構,如果是可判斷型別就通過tag去做相應的動作,如果是struct就遞迴,繼續去遍歷。
image.png

struct是我們的請求體(也就是父節點),子節點對應我們的每一個元素,它的型別是int64,string,struct或者其它的型別,我們通過型別去執行對應的行為(即int型別的eq=0,string型別的gt=0等)。

舉個例子,我們按照下邊這種方式去跑我們的validator:

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)

這時候它的執行流程大概長這個樣子:
image.png

擴充套件

到這裡其實基本原理我們都已經講完了,但是真正的實現肯定沒這麼簡單,這邊筆者給你們推薦一個專門的validator庫(https://github.com/go-playgro...),有興趣的讀者可以閱讀一下~

關注我們

歡迎對本系列文章感興趣的讀者訂閱我們的公眾號,關注博主下次不迷路~
image.png

相關文章