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

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

本文同時發表在https://github.com/zhangyachen/zhangyachen.github.io/issues/125

假設我們有如下結構體:

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.BufferFprintf(&buf, "[")    //寫入到Buffer的緩衝中

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

package mainimport (    "fmt"    "reflect"    "regexp"    "strings")const tagName = "validate"//郵箱驗證正則var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[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. %s\n", i+1, err.Error())    }}

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

github上其實已經有現成的驗證封裝了govalidator,支援內建支援的驗證tag和自訂驗證tag:

package mainimport (    "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: "admin@exmaple.com",    }    //自訂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: %v\n", ok)    }}

參考資料:

  • Custom struct field tags in Golang
  • Data validation in Golang
  • govalidator

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.