Go 語言程式設計 — validator 資料校驗工具

2020-09-29 11:01:13

目錄

Validator

Validator 是一個 Golang 的第三方庫,用於對資料進行校驗,常用於 API 的開發中,對使用者端發出的請求資料進行嚴格校驗,防止惡意請求。

  • Github:https://github.com/go-playground/validator

安裝:

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

使用:

import "github.com/go-playground/validator/v10"

NOTE:validator 當前最新的版本是 v10,各個版本之間有一些差異,在使用的時候要注意區分。

Quick start

validator 應用了 Golang 的 Struct Tag 和 Reflect 機制,基本思想是:在 Struct Tag 中為不同的欄位定義各自型別的約束,然後通過 Reflect 獲取這些約束的型別資訊並在校驗器中進行資料校驗。如下例:

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: "fanguiju", Age: 18}
  err := validate.Struct(u1)
  fmt.Println(err)

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

上述例子中,我們定義了結構體 User,有 Name 和 Age 成員。通過 validator 的 min 和 max 約束,分別約束了 Name 的字串長度 [6, 10],Age 的數位範圍為 [1,100]。

使用 validator 的第一步需要 New(構造)一個 「校驗器」,然後呼叫其 Struct 方法對結構體範例進行校驗。如果滿足約束則返回 nil,否則返回相應的錯誤資訊。

約束型別

特殊約束

  • -:跳過該欄位,不檢驗;
  • |:使用多個約束,只需要滿足其中一個,例如:rgb | rgba;
  • required:必選約束,不能為預設值;
  • omitempty:如果欄位未設定,則忽略它。

格式約束

  • email:限制欄位必須是郵件格式。
  • url:限制欄位必須是 URL 格式。
  • uri:限制欄位必須是 URI 格式。
  • ip、ipv4、ipv6:限制欄位必須是 IP 格式。
  • uuid:限制欄位必須是 UUID 格式。
  • datetime:限制欄位必須是 Datatime 格式。
  • json:字串值是否為有效的 JSON。
  • file:符串值是否包含有效的檔案路徑,以及該檔案是否存在於計算機上。

資料結構型別約束

  • structonly:僅驗證結構體,不驗證任何結構體欄位。Struct:validate:"structonly"
  • nostructlevel:不執行任何結構體級別的驗證。Struct:validate:"nostructlevel"
  • dive:向下延伸驗證,多層向下需要多個 dive 標記。[][]stringvalidate:"gt=0,dive,len=1,dive,required"
  • dive Keys & EndKeys:與 dive 同時使用,用於對 Map 物件的鍵的和值的驗證,keys 為鍵,endkeys 為值。map[string]string:validate:"gt=0,dive,keys,eq=1\|eq=2,endkeys,required"

範圍約束

  • numeric 約束:約束數值範圍;
  • String 約束:約束字串長度;
  • Slice、Array、Map 約束:約束其元素的個數。

Tags:

  • len:長度等於引數值,例如:len=10;
  • eq:數值等於引數值。與 len 不同,對於 String:eq 約束字串本身的值,而 len 則約束字串長度,例如:eq=10;
  • max:數值小於等於引數值。
  • min:數值大於等於引數值。
  • ne:不等於引數值。
  • gt:大於引數值。
  • gte:大於等於引數值。
  • lt:小於引數值。
  • lte:小於等於引數值。
  • oneof:只能是列舉值中的一個,這些值必須是數值或字串,以空格分隔,如果字串中有空格,則使用單引號包圍。例如:oneof=red green。

字串約束

下面列舉常用的字串約束:

  • contains=:包含引數子串,例如:contains=email;
  • containsany:包含引數中任意的 UNICODE 字元,例如:containsany=abcd;
  • containsrune:包含參數列示的 rune 字元,例如:containsrune=☻;
  • excludes:不包含引數子串,例如:excludes=email;
  • excludesall:不包含引數中任意的 UNICODE 字元,例如:excludesall=abcd;
  • excludesrune:不包含參數列示的 rune 字元,例如:excludesrune=☻;
  • startswith:以引數子串為字首,例如:startswith=hello;
  • endswith:以引數子串為字尾,例如:endswith=bye。
  • numeric:限制字串值只包含基本的數值。

唯一性約束

唯一性(unique)約束,對不同型別的處理如下:

  • 對於 Slice、Array,unique 約束沒有重複的元素;
  • 對於 Map,unique 約束沒有重複的值;
  • 對於元素型別為結構體的 Slice,unique 約束結構體範例的某個欄位不重複,通過 unqiue=field 可以指定這個欄位名。

跨欄位約束

validator 允許定義跨欄位的約束,即:約束某個欄位與其他欄位之間的關係。這種約束實際上分為兩種:

  1. 一種是引數欄位就是同一個結構體中的平級欄位。
  2. 另一種是引數欄位為結構中其他欄位的欄位。

約束語法很簡單,如果是約束同一個結構中的欄位,則在基礎的 Tags 後面新增一個 field 字尾,例如:eqfield 定義欄位間的相等(eq)約束。如果是更深層次的欄位,在 field 之前還需要加上 cs(Cross-Struct),eq 就變為了 eqcsfield。

範例:

type RegisterForm struct {
	Name      string `validate:"min=2"`
	Age       int    `validate:"min=18"`
	Password  string `validate:"min=10"`
	Password2 string `validate:"eqfield=Password"`
}

即:他們組成就是 「比較符號 + 是否跨 Struct(cross struct) + field」:

  • eqfield=Field:必須等於 Field 的值。
  • nefield=Field:必須不等於 Field 的值。
  • gtfield=Field:必須大於 Field 的值。
  • gtefield=Field: 必須大於等於 Field 的值。
  • ltfield=Field:必須小於 Field 的值。
  • ltefield=Field:必須小於等於 Field 的值。
  • eqcsfield=Other.Field:必須等於 struct Other 中 Field 的值。
  • necsfield=Other.Field:必須不等於 struct Other 中 Field 的值。
  • gtcsfield=Other.Field:必須大於 struct Other 中 Field 的值;
  • gtecsfield=Other.Field:必須大於等於 struct Other 中 Field 的值。
  • ltcsfield=Other.Field:必須小於 struct Other 中 Field 的值。
  • ltecsfield=Other.Field:必須小於等於 struct Other 中 Field 的值。

另外還有幾個挺有用的 Tag:

  • required_with=Field1 Field2:在 Field1 或者 Field2 存在時,必須;
  • required_with_all=Field1 Field2:在 Field1 與 Field2 都存在時,必須;
  • required_without=Field1 Field2:在 Field1 或者 Field2 不存在時,必須;
  • required_without_all=Field1 Field2:在 Field1 與 Field2 都存在時,必須;

自定義約束

除了使用 validator 提供的內建約束外,還可以定義自己的約束。首先定義一個型別為 func (validator.FieldLevel) bool 的函數檢查約束是否滿足,可以通過 FieldLevel 取出要檢查的欄位的資訊。然後,呼叫校驗器的 RegisterValidation() 方法將該約束註冊到指定的名字上。最後我們就可以在結構體中使用該約束了。

範例:

type RegisterForm struct {
  Name string `validate:"palindrome"`
  Age  int    `validate:"min=18"`
}

func reverseString(s string) string {
  runes := []rune(s)
  for from, to := 0, len(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)
  }
}

錯誤處理

validator 返回的錯誤有兩種,一種是引數錯誤,一種是校驗錯誤,它們都實現了 error 介面。

  • 引數錯誤時,返回 InvalidValidationError 型別;
  • 校驗錯誤時,返回 ValidationErrors 型別。ValidationErrors 是一個錯誤切片,儲存了每個欄位違反的每個約束資訊。

所以 validator 校驗返回的結果只有 3 種情況:

  • nil:沒有錯誤;
  • InvalidValidationError:輸入引數錯誤;
  • ValidationErrors:欄位違反約束。

我們可以在程式中判斷 err != nil 時,可以依次將 err 轉換為 InvalidValidationError 和 ValidationErrors 以獲取更詳細的資訊:

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(1, 2, "eqfield")
  processErr(err)
}

中文錯誤資訊

需要安裝兩個包:

go get github.com/go-playground/universal-translator
go get github.com/go-playground/locales

範例:

package main

import (
	"fmt"
	"github.com/go-playground/locales/zh"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

type Users struct {
	Name	string `form:"name" json:"name" validate:"required"`
	Age		uint8  `form:"age" json:"age" validate:"required,gt=18"`
	Passwd  string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
	Code    string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {
	users := &Users{
		Name:      "admin",
		Age:        12,
		Passwd:     "123",
		Code:       "123456",
	}

	// 中文翻譯器
	uni := ut.New(zh.New())
	trans, _ := uni.GetTranslator("zh")

	// 校驗器
	validate := validator.New()

	// 註冊翻譯器到校驗器
	err := zh_translations.RegisterDefaultTranslations(validate, trans)
	if err!=nil {
		fmt.Println(err)
	}
	err = validate.Struct(users)
	if err != nil {
		for _, err := range err.(validator.ValidationErrors) {
			fmt.Println(err.Translate(trans))
			return
		}
	}
	return
}

參考檔案

https://blog.csdn.net/qq_26273559/article/details/107164846