編程學(xué)習(xí)網(wǎng) > 編程語(yǔ)言 > go > Go 每日一庫(kù)之 validator:Go最優(yōu)秀的驗(yàn)證庫(kù)
2020
04-10

Go 每日一庫(kù)之 validator:Go最優(yōu)秀的驗(yàn)證庫(kù)



簡(jiǎn)介

今天我們來(lái)介紹一個(gè)非常實(shí)用的庫(kù)——validator。validator用于對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)。在 Web 開(kāi)發(fā)中,對(duì)用戶(hù)傳過(guò)來(lái)的數(shù)據(jù)我們都需要進(jìn)行嚴(yán)格校驗(yàn),防止用戶(hù)的惡意請(qǐng)求。例如日期格式,用戶(hù)年齡,性別等必須是正常的值,不能隨意設(shè)置。

快速使用

先安裝:

go get gopkg.in/go-playground/validator.v10

后使用:

package main import (
  "fmt"   "gopkg.in/go-playground/validator.v10" ) type User struct {
  Name string `validate:"min=6,max=10"`   Age  int    `validate:"min=1,max=100"` } func main() {
  validate := validator.New()

  u1 := User{Name: "lidajun", Age: 18}
  err := validate.Struct(u1)
  fmt.Println(err)

  u2 := User{Name: "dj", Age: 101}
  err = validate.Struct(u2)
  fmt.Println(err)
}

validator在結(jié)構(gòu)體標(biāo)簽(struct tag)中定義字段的約束。使用validator驗(yàn)證數(shù)據(jù)之前,我們需要調(diào)用validator.New()創(chuàng)建一個(gè)驗(yàn)證器,這個(gè)驗(yàn)證器可以指定選項(xiàng)、添加自定義約束,然后通過(guò)調(diào)用它的Struct()方法來(lái)驗(yàn)證各種結(jié)構(gòu)對(duì)象的字段是否符合定義的約束。

在上面代碼中,我們定義了一個(gè)結(jié)構(gòu)體User,User有名稱(chēng)Name字段和年齡Age字段。通過(guò)min和max約束,我們?cè)O(shè)置Name的字符串長(zhǎng)度為[6,10]之間,Age的范圍為[1,100]。

第一個(gè)對(duì)象Name和Age字段都滿(mǎn)足約束,故Struct()方法返回nil錯(cuò)誤。第二個(gè)對(duì)象的Name字段值為dj,長(zhǎng)度 2,小于最小值min,Age字段值為 101,大于最大值max,故返回錯(cuò)誤:

<nil>
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'max' tag

錯(cuò)誤信息比較好理解,User.Name違反了min約束,User.Age違反了max約束,一眼就能看出問(wèn)題所在。

注意:

  • validator已經(jīng)更新迭代了很多版本,當(dāng)前最新的版本是v10,各個(gè)版本之間有一些差異,大家平時(shí)在使用和閱讀代碼時(shí)要注意區(qū)分。我這里使用最新的版本v10作為演示版本;
  • 字符串長(zhǎng)度和數(shù)值的范圍都可以通過(guò)min和max來(lái)約束。

約束

validator提供了非常豐富的約束可供使用,下面依次來(lái)介紹。

范圍約束

我們上面已經(jīng)看到了使用min和max來(lái)約束字符串的長(zhǎng)度或數(shù)值的范圍,下面再介紹其它的范圍約束。范圍約束的字段類(lèi)型有以下幾種:

  • 對(duì)于數(shù)值,則約束其值;
  • 對(duì)于字符串,則約束其長(zhǎng)度;
  • 對(duì)于切片、數(shù)組和map,則約束其長(zhǎng)度。

下面如未特殊說(shuō)明,則是根據(jù)上面各個(gè)類(lèi)型對(duì)應(yīng)的值與參數(shù)值比較。

  • len:等于參數(shù)值,例如len=10;
  • max:小于等于參數(shù)值,例如max=10;
  • min:大于等于參數(shù)值,例如min=10;
  • eq:等于參數(shù)值,注意與len不同。對(duì)于字符串,eq約束字符串本身的值,而len約束字符串長(zhǎng)度。例如eq=10;
  • ne:不等于參數(shù)值,例如ne=10;
  • gt:大于參數(shù)值,例如gt=10;
  • gte:大于等于參數(shù)值,例如gte=10;
  • lt:小于參數(shù)值,例如lt=10;
  • lte:小于等于參數(shù)值,例如lte=10;
  • oneof:只能是列舉出的值其中一個(gè),這些值必須是數(shù)值或字符串,以空格分隔,如果字符串中有空格,將字符串用單引號(hào)包圍,例如oneof=red green。

大部分還是比較直觀的,我們通過(guò)一個(gè)例子看看其中幾個(gè)約束如何使用:

type User struct {
  Name    string    `validate:"ne=admin"`   Age     int       `validate:"gte=18"`   Sex     string    `validate:"oneof=male female"`   RegTime time.Time `validate:"lte"` } func main() {
  validate := validator.New()

  u1 := User{Name: "dj", Age: 18, Sex: "male", RegTime: time.Now().UTC()}
  err := validate.Struct(u1)
  if err != nil {
    fmt.Println(err)
  }

  u2 := User{Name: "admin", Age: 15, Sex: "none", RegTime: time.Now().UTC().Add(1 * time.Hour)}
  err = validate.Struct(u2)
  if err != nil {
    fmt.Println(err)
  }
}

上面例子中,我們定義了User對(duì)象,為它的 4 個(gè)字段分別設(shè)置了約束:

  • Name:字符串不能是admin;
  • Age:必須大于等于 18,未成年人禁止入內(nèi);
  • Sex:性別必須是male和female其中一個(gè);
  • RegTime:注冊(cè)時(shí)間必須小于當(dāng)前的 UTC 時(shí)間,注意如果字段類(lèi)型是time.Time,使用gt/gte/lt/lte等約束時(shí)不用指定參數(shù)值,默認(rèn)與當(dāng)前的 UTC 時(shí)間比較。

同樣地,第一個(gè)對(duì)象的字段都是合法的,校驗(yàn)通過(guò)。第二個(gè)對(duì)象的 4 個(gè)字段都非法,通過(guò)輸出信息很好定錯(cuò)誤位置:

Key: 'User.Name' Error:Field validation for 'Name' failed on the 'ne' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'gte' tag
Key: 'User.Sex' Error:Field validation for 'Sex' failed on the 'oneof' tag
Key: 'User.RegTime' Error:Field validation for 'RegTime' failed on the 'lte' tag

跨字段約束

validator允許定義跨字段的約束,即該字段與其他字段之間的關(guān)系。這種約束實(shí)際上分為兩種,一種是參數(shù)字段就是同一個(gè)結(jié)構(gòu)中的平級(jí)字段,另一種是參數(shù)字段為結(jié)構(gòu)中其他字段的字段。約束語(yǔ)法很簡(jiǎn)單,要想使用上面的約束語(yǔ)義,只需要稍微修改一下。例如相等約束(eq),如果是約束同一個(gè)結(jié)構(gòu)中的字段,則在后面添加一個(gè)field,使用eqfield定義字段間的相等約束。如果是更深層次的字段,在field之前還需要加上cs(可以理解為cross-struct),eq就變?yōu)閑qcsfield。它們的參數(shù)值都是需要比較的字段名,內(nèi)層的還需要加上字段的類(lèi)型:

eqfield=ConfirmPassword
eqcsfield=InnerStructField.Field

看示例:

type RegisterForm struct {
  Name      string `validate:"min=2"`   Age       int    `validate:"min=18"`   Password  string `validate:"min=10"`   Password2 string `validate:"eqfield=Password"` } func main() {
  validate := validator.New()

  f1 := RegisterForm{
    Name:      "dj",
    Age:       18,
    Password:  "1234567890",
    Password2: "1234567890",
  }
  err := validate.Struct(f1)
  if err != nil {
    fmt.Println(err)
  }

  f2 := RegisterForm{
    Name:      "dj",
    Age:       18,
    Password:  "1234567890",
    Password2: "123",
  }
  err = validate.Struct(f2)
  if err != nil {
    fmt.Println(err)
  }
}

我們定義了一個(gè)簡(jiǎn)單的注冊(cè)表單結(jié)構(gòu),使用eqfield約束其兩次輸入的密碼必須相等。第一個(gè)對(duì)象滿(mǎn)足約束,第二個(gè)對(duì)象兩次密碼明顯不等。程序輸出:

Key: 'RegisterForm.Password2' Error:Field validation for 'Password2' failed on the 'eqfield' tag

字符串

validator中關(guān)于字符串的約束有很多,這里介紹幾個(gè):

  • contains=:包含參數(shù)子串,例如contains=email;
  • containsany:包含參數(shù)中任意的 UNICODE 字符,例如containsany=abcd;
  • containsrune:包含參數(shù)表示的 rune 字符,例如containsrune=?;
  • excludes:不包含參數(shù)子串,例如excludes=email;
  • excludesall:不包含參數(shù)中任意的 UNICODE 字符,例如excludesall=abcd;
  • excludesrune:不包含參數(shù)表示的 rune 字符,excludesrune=?;
  • startswith:以參數(shù)子串為前綴,例如startswith=hello;
  • endswith:以參數(shù)子串為后綴,例如endswith=bye。

看示例:

type User struct {
  Name string `validate:"containsrune=?"`   Age  int    `validate:"min=18"` } func main() {
  validate := validator.New()

  u1 := User{"d?j"18}
  err := validate.Struct(u1)
  if err != nil {
    fmt.Println(err)
  }

  u2 := User{"dj"18}
  err = validate.Struct(u2)
  if err != nil {
    fmt.Println(err)
  }
}

限制Name字段必須包含 UNICODE 字符?。

唯一性

使用unqiue來(lái)指定唯一性約束,對(duì)不同類(lèi)型的處理如下:

  • 對(duì)于數(shù)組和切片,unique約束沒(méi)有重復(fù)的元素;
  • 對(duì)于map,unique約束沒(méi)有重復(fù)的;
  • 對(duì)于元素類(lèi)型為結(jié)構(gòu)體的切片,unique約束結(jié)構(gòu)體對(duì)象的某個(gè)字段不重復(fù),通過(guò)unqiue=field指定這個(gè)字段名。

例如:

type User struct {
  Name    string   `validate:"min=2"`   Age     int      `validate:"min=18"`   Hobbies []string `validate:"unique"`   Friends []User   `validate:"unique=Name"` } func main() {
  validate := validator.New()

  f1 := User{
    Name: "dj2",
    Age:  18,
  }
  f2 := User{
    Name: "dj3",
    Age:  18,
  }

  u1 := User{
    Name:    "dj",
    Age:     18,
    Hobbies: []string{"pingpong""chess""programming"},
    Friends: []User{f1, f2},
  }
  err := validate.Struct(u1)
  if err != nil {
    fmt.Println(err)
  }

  u2 := User{
    Name:    "dj",
    Age:     18,
    Hobbies: []string{"programming""programming"},
    Friends: []User{f1, f1},
  }
  err = validate.Struct(u2)
  if err != nil {
    fmt.Println(err)
  }
}

我們限制愛(ài)好Hobbies中不能有重復(fù)元素,好友Friends的各個(gè)元素不能有同樣的名字Name。第一個(gè)對(duì)象滿(mǎn)足約束,第二個(gè)對(duì)象的Hobbies字段包含了重復(fù)的"programming",F(xiàn)riends字段中兩個(gè)元素的Name字段都是dj2。程序輸出:

Key: 'User.Hobbies' Error:Field validation for 'Hobbies' failed on the 'unique' tag
Key: 'User.Friends' Error:Field validation for 'Friends' failed on the 'unique' tag

郵件

通過(guò)email限制字段必須是郵件格式:

type User struct {
  Name  string `validate:"min=2"`   Age   int    `validate:"min=18"`   Email string `validate:"email"` } func main() {
  validate := validator.New()

  u1 := User{
    Name:  "dj",
    Age:   18,
    Email: "dj@example.com",
  }
  err := validate.Struct(u1)
  if err != nil {
    fmt.Println(err)
  }

  u2 := User{
    Name:  "dj",
    Age:   18,
    Email: "djexample.com",
  }
  err = validate.Struct(u2)
  if err != nil {
    fmt.Println(err)
  }
}

上面我們約束Email字段必須是郵件的格式,第一個(gè)對(duì)象滿(mǎn)足約束,第二個(gè)對(duì)象不滿(mǎn)足,程序輸出:

Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag

特殊

有一些比較特殊的約束:

  • -:跳過(guò)該字段,不檢驗(yàn);
  • |:使用多個(gè)約束,只需要滿(mǎn)足其中一個(gè),例如rgb|rgba;
  • required:字段必須設(shè)置,不能為默認(rèn)值;
  • omitempty:如果字段未設(shè)置,則忽略它。

其他

validator提供了大量的、各個(gè)方面的、豐富的約束,如ASCII/UNICODE字母、數(shù)字、十六進(jìn)制、十六進(jìn)制顏色值、大小寫(xiě)、RBG 顏色值,HSL 顏色值、HSLA 顏色值、JSON 格式、文件路徑、URL、base64 編碼串、ip 地址、ipv4、ipv6、UUID、經(jīng)緯度等等等等等等等等等等等。限于篇幅這里就不一一介紹了。感興趣自行去文檔中挖掘。

VarWithValue方法

在一些很簡(jiǎn)單的情況下,我們僅僅想對(duì)兩個(gè)變量進(jìn)行比較,如果每次都要先定義結(jié)構(gòu)和tag就太繁瑣了。validator提供了VarWithValue()方法,我們只需要傳入要驗(yàn)證的兩個(gè)變量和約束即可

func main() {
  name1 := "dj"   name2 := "dj2"   validate := validator.New()
  fmt.Println(validate.VarWithValue(name1, name2, "eqfield"))

  fmt.Println(validate.VarWithValue(name1, name2, "nefield"))
}

自定義約束

除了使用validator提供的約束外,還可以定義自己的約束。例如現(xiàn)在有個(gè)奇葩的需求,產(chǎn)品同學(xué)要求用戶(hù)必須使用回文串作為用戶(hù)名,我們可以自定義這個(gè)約束:

type RegisterForm struct {
  Name string `validate:"palindrome"`   Age  int    `validate:"min=18"` } func reverseString(s string) string {
  runes := []rune(s)
  for from, to := 0len(runes)-1; from < to; from, to = from+1, to-1 {
    runes[from], runes[to] = runes[to], runes[from]
  }

  return string(runes)
} func CheckPalindrome(fl validator.FieldLevel) bool {
  value := fl.Field().String()
  return value == reverseString(value)
} func main() {
  validate := validator.New()
  validate.RegisterValidation("palindrome", CheckPalindrome)

  f1 := RegisterForm{
    Name: "djd",
    Age:  18,
  }
  err := validate.Struct(f1)
  if err != nil {
    fmt.Println(err)
  }

  f2 := RegisterForm{
    Name: "dj",
    Age:  18,
  }
  err = validate.Struct(f2)
  if err != nil {
    fmt.Println(err)
  }
}

首先定義一個(gè)類(lèi)型為func (validator.FieldLevel) bool的函數(shù)檢查約束是否滿(mǎn)足,可以通過(guò)FieldLevel取出要檢查的字段的信息。然后,調(diào)用驗(yàn)證器的RegisterValidation()方法將該約束注冊(cè)到指定的名字上。最后我們就可以在結(jié)構(gòu)體中使用該約束。上面程序中,第二個(gè)對(duì)象不滿(mǎn)足約束palindrome,輸出:

Key: 'RegisterForm.Name' Error:Field validation for 'Name' failed on the 'palindrome' tag

錯(cuò)誤處理

在上面的例子中,校驗(yàn)失敗時(shí)我們僅僅只是輸出返回的錯(cuò)誤。其實(shí),我們可以進(jìn)行更精準(zhǔn)的處理。validator返回的錯(cuò)誤實(shí)際上只有兩種,一種是參數(shù)錯(cuò)誤,一種是校驗(yàn)錯(cuò)誤。參數(shù)錯(cuò)誤時(shí),返回InvalidValidationError類(lèi)型;校驗(yàn)錯(cuò)誤時(shí)返回ValidationErrors,它們都實(shí)現(xiàn)了error接口。而且ValidationErrors是一個(gè)錯(cuò)誤切片,它保存了每個(gè)字段違反的每個(gè)約束信息:

// src/gopkg.in/validator.v10/errors.go type InvalidValidationError struct {
  Type reflect.Type
} // Error returns InvalidValidationError message func (e *InvalidValidationError) Error() string {
  if e.Type == nil {
    return "validator: (nil)"   }

  return "validator: (nil " + e.Type.String() + ")" } type ValidationErrors []FieldError func (ve ValidationErrors) Error() string {
  buff := bytes.NewBufferString("")
  var fe *fieldError

  for i := 0; i < len(ve); i++ {
    fe = ve[i].(*fieldError)
    buff.WriteString(fe.Error())
    buff.WriteString("\n")
  }
  return strings.TrimSpace(buff.String())
}

所以validator校驗(yàn)返回的結(jié)果只有 3 種情況:

  • nil:沒(méi)有錯(cuò)誤;
  • InvalidValidationError:輸入?yún)?shù)錯(cuò)誤;
  • ValidationErrors:字段違反約束。

我們可以在程序中判斷err != nil時(shí),依次將err轉(zhuǎn)換為InvalidValidationError和ValidationErrors以獲取更詳細(xì)的信息:

func processErr(err error) {
  if err == nil {
    return   }

  invalid, ok := err.(*validator.InvalidValidationError)
  if ok {
    fmt.Println("param error:", invalid)
    return   }

  validationErrs := err.(validator.ValidationErrors)
  for _, validationErr := range validationErrs {
    fmt.Println(validationErr)
  }
} func main() {
  validate := validator.New()

  err := validate.Struct(1)
  processErr(err)

  err = validate.VarWithValue(12"eqfield")
  processErr(err)
}

總結(jié)

validator功能非常豐富,使用較為簡(jiǎn)單方便。本文介紹的約束只是其中的冰山一角。它的應(yīng)用非常廣泛,建議了解一下。

掃碼二維碼 獲取免費(fèi)視頻學(xué)習(xí)資料

Python編程學(xué)習(xí)

查 看2022高級(jí)編程視頻教程免費(fèi)獲取