簡(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 := 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) } }
首先定義一個(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(1, 2, "eqfield") processErr(err) }
總結(jié)
validator功能非常豐富,使用較為簡(jiǎn)單方便。本文介紹的約束只是其中的冰山一角。它的應(yīng)用非常廣泛,建議了解一下。
掃碼二維碼 獲取免費(fèi)視頻學(xué)習(xí)資料
- 本文固定鏈接: http://phpxs.com/post/7241/
- 轉(zhuǎn)載請(qǐng)注明:轉(zhuǎn)載必須在正文中標(biāo)注并保留原文鏈接
- 掃碼: 掃上方二維碼獲取免費(fèi)視頻資料