1. 程式人生 > 其它 >golang 如何驗證struct欄位的資料格式

golang 如何驗證struct欄位的資料格式

假設我們有如下結構體:

type User struct {
    Id    int    
    Name  string 
    Bio   string 
    Email string 
}

我們需要對結構體內的欄位進行驗證合法性:

▪ Id的值在某一個範圍內。 ▪ Name的長度在某一個範圍內。 ▪ Email格式正確。

我們可能會這麼寫:

user := User{
        Id:    0,
        Name:  "superlongstring",
        Bio:   "",
        Email: "foobar",
}

if user.Id < 1 && user.Id > 1000 {
    return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
    return false
}
if !validateEmail(user.Email) {
    return false
}

這樣的話程式碼比較冗餘,而且如果結構體新加欄位,還需要再修改驗證函式再加一段if判斷。這樣程式碼比較冗餘。我們可以藉助golang的structTag來解決上述的問題:

type User struct {
    Id    int    `validate:"number,min=1,max=1000"`
    Name  string `validate:"string,min=2,max=10"`
    Bio   string `validate:"string"`
    Email string `validate:"email"`
}

validate:"number,min=1,max=1000"

就是structTag。如果對這個比較陌生的話,看看下面這個:

type User struct {
    Id        int       `json:"id"`
    Name      string    `json:"name"`
    Bio       string    `json:"about,omitempty"`
    Active    bool      `json:"active"`
    Admin     bool      `json:"-"`
    CreatedAt time.Time `json:"created_at"`
}

寫過golang的基本都用過json:xxx

這個用法,json:xxx其實也是一個structTag,只不過這是golang幫你實現好特定用法的structTag。而validate:"number,min=1,max=1000"是我們自定義的structTag。

實現思路

我們定義一個介面Validator,定義一個方法Validate。再定義有具體意義的驗證器例如StringValidatorNumberValidatorEmailValidator來實現介面Validator

這裡為什麼要使用介面?假設我們不使用介面程式碼會怎麼寫?

if tagIsOfNumber(){
        validator := NumberValidator{}
}else if tagIsOfString() {
        validator := StringValidator{}
}else if tagIsOfEmail() {
        validator := EmailValidator{}
}else if tagIsOfDefault() {
        validator := DefaultValidator{}
}

這樣的話判斷邏輯不能寫在一個函式中,因為返回值validator會因為structTag的不同而不同,而且validator也不能當做函式引數做傳遞。而我們定義一個介面,所有的validator都去實現這個介面,上述的問題就能解決,而且邏輯更加清晰和緊湊。

關於介面的使用可以看下標準庫的io Writer,Writer是個interface,只有一個方法Writer:

type Writer interface {
	Write(p []byte) (n int, err error)
}

而輸出函式可以直接呼叫引數的Write方法即可,無需關心到底是寫到檔案還是寫到標準輸出:

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)      //呼叫Write方法
	p.free()
	return
}

//呼叫
Fprintf(os.Stdout, format, a...)    //標準輸出
Fprintf(os.Stderr, msg+"n", args...)   //標準錯誤輸出

var buf bytes.Buffer
Fprintf(&buf, "[")    //

言歸正傳,我們看下完整程式碼,程式碼是 Custom struct field tags in Golang 中給出的:

package main

import (
    "fmt"
    "reflect"
    "regexp"
    "strings"
)

const tagName = "validate"

//郵箱驗證正則
var mailRe = regexp.MustCompile(`A[w+-.]+@[a-zd-]+(.[a-z]+)*.[a-z]+z`)

//驗證介面
type Validator interface {
    Validate(interface{}) (bool, error)
}

type DefaultValidator struct {
}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
    return true, nil
}

type StringValidator struct {
    Min int
    Max int
}

func (v StringValidator) Validate(val interface{}) (bool, error) {
    l := len(val.(string))

    if l == 0 {
        return false, fmt.Errorf("cannot be blank")
    }

    if l < v.Min {
        return false, fmt.Errorf("should be at least %v chars long", v.Min)
    }

    if v.Max >= v.Min && l > v.Max {
        return false, fmt.Errorf("should be less than %v chars long", v.Max)
    }

    return true, nil
}


type NumberValidator struct {
    Min int
    Max int
}

func (v NumberValidator) Validate(val interface{}) (bool, error) {
    num := val.(int)

    if num < v.Min {
        return false, fmt.Errorf("should be greater than %v", v.Min)
    }

    if v.Max >= v.Min && num > v.Max {
        return false, fmt.Errorf("should be less than %v", v.Max)
    }

    return true, nil
}

type EmailValidator struct {
}

func (v EmailValidator) Validate(val interface{}) (bool, error) {
    if !mailRe.MatchString(val.(string)) {
        return false, fmt.Errorf("is not a valid email address")
    }
    return true, nil
}

func getValidatorFromTag(tag string) Validator {
    args := strings.Split(tag, ",")

    switch args[0] {
    case "number":
        validator := NumberValidator{}
        //將structTag中的min和max解析到結構體中
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
        return validator
    case "string":
        validator := StringValidator{}
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
        return validator
    case "email":
        return EmailValidator{}
    }

    return DefaultValidator{}
}

func validateStruct(s interface{}) []error {
    errs := []error{}

    v := reflect.ValueOf(s)

    for i := 0; i < v.NumField(); i++ {
        //利用反射獲取structTag
        tag := v.Type().Field(i).Tag.Get(tagName)

        if tag == "" || tag == "-" {
            continue
        }

        validator := getValidatorFromTag(tag)

        valid, err := validator.Validate(v.Field(i).Interface())
        if !valid && err != nil {
            errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
        }
    }

    return errs
}

type User struct {
    Id    int    `validate:"number,min=1,max=1000"`
    Name  string `validate:"string,min=2,max=10"`
    Bio   string `validate:"string"`
    Email string `validate:"email"`
}

func main() {
    user := User{
        Id:    0,
        Name:  "superlongstring",
        Bio:   "",
        Email: "foobar",
    }

    fmt.Println("Errors:")
    for i, err := range validateStruct(user) {
        fmt.Printf("t%d. %sn", i+1, err.Error())
    }
}

程式碼很好理解,結構也很清晰,不做過多解釋了^_^

github上其實已經有現成的驗證包了govalidator,支援內建支援的驗證tag和自定義驗證tag:

package main

import (
    "github.com/asaskevich/govalidator"
    "fmt"
    "strings"
)

type Server struct {
    ID         string `valid:"uuid,required"`
    Name       string `valid:"machine_id"`
    HostIP     string `valid:"ip"`
    MacAddress string `valid:"mac,required"`
    WebAddress string `valid:"url"`
    AdminEmail string `valid:"email"`
}

func main() {
    server := &Server{
        ID:         "123e4567-e89b-12d3-a456-426655440000",
        Name:       "IX01",
        HostIP:     "127.0.0.1",
        MacAddress: "01:23:45:67:89:ab",
        WebAddress: "www.example.com",
        AdminEmail: "[email protected]",
    }

    //自定義tag驗證函式
    govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
        return strings.HasPrefix(str, "IX")
    })

    if ok, err := govalidator.ValidateStruct(server); err != nil {
        panic(err)
    } else {
        fmt.Printf("OK: %vn", ok)
    }
}

參考資料:

▪ Custom struct field tags in Golang https://sosedoff.com/2016/07/16/golang-struct-tags.html

▪ Data validation in Golang http://blog.ralch.com/tutorial/golang-model-validation/

▪ govalidator https://github.com/asaskevich/govalidator


關於作者

作者: 張雅宸 來源: Github