Golang 經(jīng)典校驗(yàn)庫(kù) validator 用法解析
開篇
今天繼續(xù)我們的 Golang 經(jīng)典開源庫(kù)學(xué)習(xí)之旅,這篇文章的主角是 validator,Golang 中經(jīng)典的校驗(yàn)庫(kù),它可以讓開發(fā)者可以很便捷地通過 tag 來控制對(duì)結(jié)構(gòu)體字段的校驗(yàn),使用面非常廣泛。
本來打算一節(jié)收尾,越寫越發(fā)現(xiàn) validator 整體復(fù)雜度還是很高的,而且支持了很多場(chǎng)景??刹鸾獾乃悸泛芏?,于是打算分成兩篇文章來講。這篇我們會(huì)先來了解 validator 的用法,下一篇我們會(huì)關(guān)注實(shí)現(xiàn)的思路和源碼解析。
validator
Package validator implements value validations for structs and individual fields based on tags.
validator 是一個(gè)結(jié)構(gòu)體參數(shù)驗(yàn)證器。
它提供了【基于 tag 對(duì)結(jié)構(gòu)體以及單獨(dú)屬性的校驗(yàn)?zāi)芰Α?。?jīng)典的 gin 框架就是用了 validator 作為默認(rèn)的校驗(yàn)器。它的能力能夠幫助開發(fā)者最大程度地減少【基礎(chǔ)校驗(yàn)】的代碼,你只需要一個(gè) tag 就能完成校驗(yàn)。完整的文檔參照 這里。
目前 validator 最新版本已經(jīng)升級(jí)到了 v10,我們可以用
go get github.com/go-playground/validator/v10
添加依賴后,import 進(jìn)來即可
import "github.com/go-playground/validator/v10"
我們先來看一個(gè)簡(jiǎn)單的例子,了解 validator 能怎樣幫助開發(fā)者完成校驗(yàn)。
package main
import (
"fmt"
"github.com/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)
}
這里我們有一個(gè) User 結(jié)構(gòu)體,我們希望 Name 這個(gè)字符串長(zhǎng)度在 [6, 10] 這個(gè)區(qū)間內(nèi),并且希望 Age 這個(gè)數(shù)字在 [1, 100] 區(qū)間內(nèi)。就可以用上面這個(gè) tag。
校驗(yàn)的時(shí)候只需要三步:
- 調(diào)用
validator.New()初始化一個(gè)校驗(yàn)器; - 將【待校驗(yàn)的結(jié)構(gòu)體】傳入我們的校驗(yàn)器的
Struct方法中; - 校驗(yàn)返回的 error 是否為 nil 即可。
上面的例子中,lidajun 長(zhǎng)度符合預(yù)期,18 這個(gè) Age 也在區(qū)間內(nèi),預(yù)期 err 為 nil。而第二個(gè)用例 Name 和 Age 都在區(qū)間外。我們運(yùn)行一下看看結(jié)果:
<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
這里我們也可以看到,validator 返回的報(bào)錯(cuò)信息包含了 Field 名稱 以及 tag 名稱,這樣我們也容易判斷哪個(gè)校驗(yàn)沒過。
如果沒有 tag,我們自己手寫的話,還需要這樣處理:
func validate(u User) bool {
if u.Age < 1 || u.Age > 100 {
return false
}
if len(u.Name) < 6 || len(u.Name) > 10 {
return false
}
return true
}
乍一看好像區(qū)別不大,其實(shí)一旦結(jié)構(gòu)體屬性變多,校驗(yàn)規(guī)則變復(fù)雜,這個(gè)校驗(yàn)函數(shù)的代價(jià)立刻會(huì)上升,另外你還要顯示的處理報(bào)錯(cuò)信息,以達(dá)到上面這樣清晰的效果(這個(gè)手寫的示例代碼只返回了一個(gè) bool,不好判斷是哪個(gè)沒過)。
越是大結(jié)構(gòu)體,越是規(guī)則復(fù)雜,validator 的收益就越高。我們還可以把 validator 放到中間件里面,對(duì)所有請(qǐng)求加上校驗(yàn),用的越多,效果越明顯。
其實(shí)筆者個(gè)人使用經(jīng)驗(yàn)來看,validator 帶來的另外兩個(gè)好處在于:
- 因?yàn)樾枰?jīng)常使用校驗(yàn)?zāi)芰?,養(yǎng)成了習(xí)慣,每定義一個(gè)結(jié)構(gòu),都事先想好每個(gè)屬性應(yīng)該有哪些約束,促使開發(fā)者思考自己的模型。這一點(diǎn)非常重要,很多時(shí)候我們就是太隨意定義一些結(jié)構(gòu),沒有對(duì)應(yīng)的校驗(yàn),結(jié)果導(dǎo)致各種臟數(shù)據(jù),把校驗(yàn)邏輯一路下沉;
- 有了 tag 來描述約束規(guī)則,讓結(jié)構(gòu)體本身更容易理解,可讀性,可維護(hù)性提高。一看結(jié)構(gòu)體,掃幾眼 tag 就知道業(yè)務(wù)對(duì)它的預(yù)期。
這兩個(gè)點(diǎn)雖然比較【意識(shí)流】,但在開發(fā)習(xí)慣上還是很重要的。
好了,到目前只是淺嘗輒止,下面我們結(jié)合示例看看 validator 到底提供了哪些能力。
使用方法
我們上一節(jié)舉的例子就是最簡(jiǎn)單的場(chǎng)景,在一個(gè) struct 中定義好 validate:"xxx" tag,然后調(diào)用校驗(yàn)器的 err := validate.Struct(user) 方法來校驗(yàn)。
這一節(jié)我們結(jié)合實(shí)例來看看最常用的場(chǎng)景下,我們會(huì)怎樣用 validator:
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
// User contains user information
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
// Address houses a users address information
type Address struct {
Street string `validate:"required"`
City string `validate:"required"`
Planet string `validate:"required"`
Phone string `validate:"required"`
}
// use a single instance of Validate, it caches struct info
var validate *validator.Validate
func main() {
validate = validator.New()
validateStruct()
validateVariable()
}
func validateStruct() {
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
}
user := &User{
FirstName: "Badger",
LastName: "Smith",
Age: 135,
Email: "Badger.Smith@gmail.com",
FavouriteColor: "#000-",
Addresses: []*Address{address},
}
// returns nil or ValidationErrors ( []FieldError )
err := validate.Struct(user)
if err != nil {
// this check is only needed when your code could produce
// an invalid value for validation such as interface with nil
// value most including myself do not usually have code like this.
if _, ok := err.(*validator.InvalidValidationError); ok {
fmt.Println(err)
return
}
for _, err := range err.(validator.ValidationErrors) {
fmt.Println(err.Namespace())
fmt.Println(err.Field())
fmt.Println(err.StructNamespace())
fmt.Println(err.StructField())
fmt.Println(err.Tag())
fmt.Println(err.ActualTag())
fmt.Println(err.Kind())
fmt.Println(err.Type())
fmt.Println(err.Value())
fmt.Println(err.Param())
fmt.Println()
}
// from here you can create your own error messages in whatever language you wish
return
}
// save user to database
}
func validateVariable() {
myEmail := "joeybloggs.gmail.com"
errs := validate.Var(myEmail, "required,email")
if errs != nil {
fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "email" tag
return
}
// email ok, move on
}
仔細(xì)觀察你會(huì)發(fā)現(xiàn),第一步永遠(yuǎn)是創(chuàng)建一個(gè)校驗(yàn)器,一個(gè) validator.New() 解決問題,后續(xù)一定要復(fù)用,內(nèi)部有緩存機(jī)制,效率比較高。
關(guān)鍵在第二步,大體上分為兩類:
- 基于結(jié)構(gòu)體調(diào)用
err := validate.Struct(user)來校驗(yàn); - 基于變量調(diào)用
errs := validate.Var(myEmail, "required,email")
結(jié)構(gòu)體校驗(yàn)這個(gè)相信看完這個(gè)實(shí)例,大家已經(jīng)很熟悉了。
變量校驗(yàn)這里很有意思,用起來確實(shí)簡(jiǎn)單,大家看 validateVariable 這個(gè)示例就 ok,但是,但是,我只有一個(gè)變量,我為啥還要用這個(gè) validator ?。?/p>
原因很簡(jiǎn)單,不要以為 validator 只能干一些及其簡(jiǎn)單的,比大小,比長(zhǎng)度,判空邏輯。這些非常基礎(chǔ)的校驗(yàn)用一個(gè) if 語(yǔ)句也搞定。
validator 支持的校驗(yàn)規(guī)則遠(yuǎn)比這些豐富的多。
我們先把前面示例的結(jié)構(gòu)體拿出來,看看支持哪些 tag:
// User contains user information
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
// Address houses a users address information
type Address struct {
Street string `validate:"required"`
City string `validate:"required"`
Planet string `validate:"required"`
Phone string `validate:"required"`
}
格式都是 validate:"xxx",這里不再說,關(guān)鍵是里面的配置。
validator 中如果你針對(duì)同一個(gè) Field,有多個(gè)校驗(yàn)項(xiàng),可以用下面兩種運(yùn)算符:
,逗號(hào)表示【與】,即每一個(gè)都需要滿足;|表示【或】,多個(gè)條件滿足一個(gè)即可。
我們一個(gè)個(gè)來看這個(gè) User 結(jié)構(gòu)體出現(xiàn)的 tag:
- required 要求必須有值,不為空;
- gte=0,lte=130 其中 gte 代表大于等于,lte 代表小于等于,這個(gè)語(yǔ)義是 [0,130] 區(qū)間;
- required, emal 不僅僅要有值,還得符合 Email 格式;
- iscolor 后面注釋也提了,這是個(gè)別名,本質(zhì)等價(jià)于 hexcolor|rgb|rgba|hsl|hsla,屬于 validator 自帶的別名能力,符合這幾個(gè)規(guī)則任一的,我們都認(rèn)為屬于表示顏色。
- required,dive,required 這個(gè) dive 大有來頭,注意這個(gè) Addresses 是個(gè) Address 數(shù)組,我們加 tag 一般只是針對(duì)單獨(dú)的數(shù)據(jù)類型,這種【容器型】的怎么辦?
這時(shí) dive 的能力就派上用場(chǎng)了。
dive 的語(yǔ)義在于告訴 validator 不要停留在我這一級(jí),而是繼續(xù)往下校驗(yàn),無(wú)論是 slice, array 還是 map,校驗(yàn)要用的 tag 就是在 dive 之后的這個(gè)。
這樣說可能不直觀,我們來看一個(gè)例子:
[][]string with validation tag "gt=0,dive,len=1,dive,required" // gt=0 will be applied to [] // len=1 will be applied to []string // required will be applied to string
第一個(gè) gt=0 適用于最外層的數(shù)組,出現(xiàn) dive 后,往下走,len=1 作為一個(gè) tag 適用于內(nèi)層的 []string,此后又出現(xiàn) dive,繼續(xù)往下走,對(duì)于最內(nèi)層的每個(gè) string,要求每個(gè)都是 required。
[][]string with validation tag "gt=0,dive,dive,required" // gt=0 will be applied to [] // []string will be spared validation // required will be applied to string
第二個(gè)例子,看看能不能理解?
其實(shí),只要記住,每次出現(xiàn) dive,都往里面走就 ok。
回到我們一開始的例子:
Addresses []*Address validate:"required,dive,required"
表示的意思是,我們要求 Addresses 這個(gè)數(shù)組是 required,此外對(duì)于每個(gè)元素,也得是 required。
內(nèi)置校驗(yàn)器
validator 對(duì)于下面六種場(chǎng)景都提供了豐富的校驗(yàn)器,放到 tag 里就能用。這里我們簡(jiǎn)單看一下:
(注:想看完整的建議參考文檔 以及倉(cāng)庫(kù) README)
1. Fields
對(duì)于結(jié)構(gòu)體各個(gè)屬性的校驗(yàn),這里可以針對(duì)一個(gè) field 與另一個(gè) field 相互比較。

2. Network
網(wǎng)絡(luò)相關(guān)的格式校驗(yàn),可以用來校驗(yàn) IP 格式,TCP, UDP, URL 等

3. Strings
字符串相關(guān)的校驗(yàn),用的非常多,比如校驗(yàn)是否是數(shù)字,大小寫,前后綴等,非常方便。

4. Formats
符合特定格式,如我們上面提到的 email,信用卡號(hào),顏色,html,base64,json,經(jīng)緯度,md5 等

5. Comparisons
比較大小,用的很多

6. Other
雜項(xiàng),各種通用能力,用的也非常多,我們上面用的 required 就在這一節(jié)。包括校驗(yàn)是否為默認(rèn)值,最大,最小等。

7. 別名
除了上面的六個(gè)大類,還包含兩個(gè)內(nèi)部封裝的別名校驗(yàn)器,我們已經(jīng)用過 iscolor,還有國(guó)家碼:

錯(cuò)誤處理
Golang 的 error 是個(gè) interface,默認(rèn)其實(shí)只提供了 Error() 這一個(gè)方法,返回一個(gè)字符串,能力比較雞肋。同樣的,validator 返回的錯(cuò)誤信息也是個(gè)字符串:
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
這樣當(dāng)然不錯(cuò),但問題在于,線上環(huán)境下,很多時(shí)候我們并不是【人工地】來閱讀錯(cuò)誤信息,這里的 error 最終是要轉(zhuǎn)化成錯(cuò)誤信息展現(xiàn)給用戶,或者打點(diǎn)上報(bào)的。
我們需要有能力解析出來,是哪個(gè)結(jié)構(gòu)體的哪個(gè)屬性有問題,哪個(gè) tag 攔截了。怎么辦?
其實(shí) validator 返回的類型底層是 validator.ValidationErrors,我們可以在判空之后,用它來進(jìn)行類型斷言,將 error 類型轉(zhuǎn)化過來再判斷:
err := validate.Struct(mystruct) validationErrors := err.(validator.ValidationErrors)
底層的結(jié)構(gòu)我們看一下:
// ValidationErrors is an array of FieldError's
// for use in custom error messages post validation.
type ValidationErrors []FieldError
// Error is intended for use in development + debugging and not intended to be a production error message.
// It allows ValidationErrors to subscribe to the Error interface.
// All information to create an error message specific to your application is contained within
// the FieldError found within the ValidationErrors array
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())
}
這里可以看到,所謂 ValidationErrors 其實(shí)一組 FieldError,所謂 FieldError 就是每一個(gè)屬性的報(bào)錯(cuò),我們的 ValidationErrors 實(shí)現(xiàn)的 func Error() string 方法,也是將各個(gè) fieldError(對(duì) FieldError 接口的默認(rèn)實(shí)現(xiàn))連接起來,最后 TrimSpace 清掉空格展示。
在我們拿到了 ValidationErrors 后,可以遍歷各個(gè) FieldError,拿到業(yè)務(wù)需要的信息,用來做日志打印/打點(diǎn)上報(bào)/錯(cuò)誤碼對(duì)照等,這里是個(gè) interface,大家各取所需即可:
// FieldError contains all functions to get error details
type FieldError interface {
// Tag returns the validation tag that failed. if the
// validation was an alias, this will return the
// alias name and not the underlying tag that failed.
//
// eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla"
// will return "iscolor"
Tag() string
// ActualTag returns the validation tag that failed, even if an
// alias the actual tag within the alias will be returned.
// If an 'or' validation fails the entire or will be returned.
//
// eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla"
// will return "hexcolor|rgb|rgba|hsl|hsla"
ActualTag() string
// Namespace returns the namespace for the field error, with the tag
// name taking precedence over the field's actual name.
//
// eg. JSON name "User.fname"
//
// See StructNamespace() for a version that returns actual names.
//
// NOTE: this field can be blank when validating a single primitive field
// using validate.Field(...) as there is no way to extract it's name
Namespace() string
// StructNamespace returns the namespace for the field error, with the field's
// actual name.
//
// eq. "User.FirstName" see Namespace for comparison
//
// NOTE: this field can be blank when validating a single primitive field
// using validate.Field(...) as there is no way to extract its name
StructNamespace() string
// Field returns the fields name with the tag name taking precedence over the
// field's actual name.
//
// eq. JSON name "fname"
// see StructField for comparison
Field() string
// StructField returns the field's actual name from the struct, when able to determine.
//
// eq. "FirstName"
// see Field for comparison
StructField() string
// Value returns the actual field's value in case needed for creating the error
// message
Value() interface{}
// Param returns the param value, in string form for comparison; this will also
// help with generating an error message
Param() string
// Kind returns the Field's reflect Kind
//
// eg. time.Time's kind is a struct
Kind() reflect.Kind
// Type returns the Field's reflect Type
//
// eg. time.Time's type is time.Time
Type() reflect.Type
// Translate returns the FieldError's translated error
// from the provided 'ut.Translator' and registered 'TranslationFunc'
//
// NOTE: if no registered translator can be found it returns the same as
// calling fe.Error()
Translate(ut ut.Translator) string
// Error returns the FieldError's message
Error() string
}
小結(jié)
今天我們了解了 validator 的用法,其實(shí)整體還是非常簡(jiǎn)潔的,我們只需要全局維護(hù)一個(gè) validator 實(shí)例,內(nèi)部會(huì)幫我們做好緩存。此后只需要把結(jié)構(gòu)體傳入,就可以完成校驗(yàn),并提供可以解析的錯(cuò)誤。
validator 的實(shí)現(xiàn)也非常精巧,只不過內(nèi)容太多,我們今天暫時(shí)覆蓋不到,更多關(guān)于Go 校驗(yàn)庫(kù)validator 的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Golang如何實(shí)現(xiàn)簡(jiǎn)易的令牌桶算法
這篇文章主要介紹了使用Golang如何實(shí)現(xiàn)簡(jiǎn)易的令牌桶算法問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
golang中package?is?not?in?GOROOT報(bào)錯(cuò)的真正解決辦法
這篇文章主要給大家介紹了關(guān)于golang中package?is?not?in?GOROOT報(bào)錯(cuò)的真正解決辦法,文中通過圖文介紹的非常詳細(xì),對(duì)同樣遇到這個(gè)問題的朋友具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-03-03
golang?gorm的預(yù)加載及軟刪硬刪的數(shù)據(jù)操作示例
這篇文章主要介紹了golang?gorm的預(yù)加載及軟刪硬刪的數(shù)據(jù)操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04

