Golang 經(jīng)典校驗庫 validator 用法解析
開篇
今天繼續(xù)我們的 Golang 經(jīng)典開源庫學(xué)習(xí)之旅,這篇文章的主角是 validator,Golang 中經(jīng)典的校驗庫,它可以讓開發(fā)者可以很便捷地通過 tag 來控制對結(jié)構(gòu)體字段的校驗,使用面非常廣泛。
本來打算一節(jié)收尾,越寫越發(fā)現(xiàn) validator 整體復(fù)雜度還是很高的,而且支持了很多場景??刹鸾獾乃悸泛芏啵谑谴蛩惴殖蓛善恼聛碇v。這篇我們會先來了解 validator 的用法,下一篇我們會關(guān)注實現(xiàn)的思路和源碼解析。
validator
Package validator implements value validations for structs and individual fields based on tags.
validator 是一個結(jié)構(gòu)體參數(shù)驗證器。
它提供了【基于 tag 對結(jié)構(gòu)體以及單獨屬性的校驗?zāi)芰Α俊=?jīng)典的 gin 框架就是用了 validator 作為默認(rèn)的校驗器。它的能力能夠幫助開發(fā)者最大程度地減少【基礎(chǔ)校驗】的代碼,你只需要一個 tag 就能完成校驗。完整的文檔參照 這里。
目前 validator 最新版本已經(jīng)升級到了 v10,我們可以用
go get github.com/go-playground/validator/v10
添加依賴后,import 進來即可
import "github.com/go-playground/validator/v10"
我們先來看一個簡單的例子,了解 validator 能怎樣幫助開發(fā)者完成校驗。
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) }
這里我們有一個 User 結(jié)構(gòu)體,我們希望 Name 這個字符串長度在 [6, 10] 這個區(qū)間內(nèi),并且希望 Age 這個數(shù)字在 [1, 100] 區(qū)間內(nèi)。就可以用上面這個 tag。
校驗的時候只需要三步:
- 調(diào)用
validator.New()
初始化一個校驗器; - 將【待校驗的結(jié)構(gòu)體】傳入我們的校驗器的
Struct
方法中; - 校驗返回的 error 是否為 nil 即可。
上面的例子中,lidajun 長度符合預(yù)期,18 這個 Age 也在區(qū)間內(nèi),預(yù)期 err 為 nil。而第二個用例 Name 和 Age 都在區(qū)間外。我們運行一下看看結(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 返回的報錯信息包含了 Field 名稱 以及 tag 名稱,這樣我們也容易判斷哪個校驗沒過。
如果沒有 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ū)別不大,其實一旦結(jié)構(gòu)體屬性變多,校驗規(guī)則變復(fù)雜,這個校驗函數(shù)的代價立刻會上升,另外你還要顯示的處理報錯信息,以達到上面這樣清晰的效果(這個手寫的示例代碼只返回了一個 bool,不好判斷是哪個沒過)。
越是大結(jié)構(gòu)體,越是規(guī)則復(fù)雜,validator 的收益就越高。我們還可以把 validator 放到中間件里面,對所有請求加上校驗,用的越多,效果越明顯。
其實筆者個人使用經(jīng)驗來看,validator 帶來的另外兩個好處在于:
- 因為需要經(jīng)常使用校驗?zāi)芰?,養(yǎng)成了習(xí)慣,每定義一個結(jié)構(gòu),都事先想好每個屬性應(yīng)該有哪些約束,促使開發(fā)者思考自己的模型。這一點非常重要,很多時候我們就是太隨意定義一些結(jié)構(gòu),沒有對應(yīng)的校驗,結(jié)果導(dǎo)致各種臟數(shù)據(jù),把校驗邏輯一路下沉;
- 有了 tag 來描述約束規(guī)則,讓結(jié)構(gòu)體本身更容易理解,可讀性,可維護性提高。一看結(jié)構(gòu)體,掃幾眼 tag 就知道業(yè)務(wù)對它的預(yù)期。
這兩個點雖然比較【意識流】,但在開發(fā)習(xí)慣上還是很重要的。
好了,到目前只是淺嘗輒止,下面我們結(jié)合示例看看 validator 到底提供了哪些能力。
使用方法
我們上一節(jié)舉的例子就是最簡單的場景,在一個 struct 中定義好 validate:"xxx"
tag,然后調(diào)用校驗器的 err := validate.Struct(user)
方法來校驗。
這一節(jié)我們結(jié)合實例來看看最常用的場景下,我們會怎樣用 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ì)觀察你會發(fā)現(xiàn),第一步永遠(yuǎn)是創(chuàng)建一個校驗器,一個 validator.New()
解決問題,后續(xù)一定要復(fù)用,內(nèi)部有緩存機制,效率比較高。
關(guān)鍵在第二步,大體上分為兩類:
- 基于結(jié)構(gòu)體調(diào)用
err := validate.Struct(user)
來校驗; - 基于變量調(diào)用
errs := validate.Var(myEmail, "required,email")
結(jié)構(gòu)體校驗這個相信看完這個實例,大家已經(jīng)很熟悉了。
變量校驗這里很有意思,用起來確實簡單,大家看 validateVariable
這個示例就 ok,但是,但是,我只有一個變量,我為啥還要用這個 validator ???
原因很簡單,不要以為 validator 只能干一些及其簡單的,比大小,比長度,判空邏輯。這些非常基礎(chǔ)的校驗用一個 if 語句也搞定。
validator 支持的校驗規(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 中如果你針對同一個 Field,有多個校驗項,可以用下面兩種運算符:
,
逗號表示【與】,即每一個都需要滿足;|
表示【或】,多個條件滿足一個即可。
我們一個個來看這個 User 結(jié)構(gòu)體出現(xiàn)的 tag:
- required 要求必須有值,不為空;
- gte=0,lte=130 其中 gte 代表大于等于,lte 代表小于等于,這個語義是 [0,130] 區(qū)間;
- required, emal 不僅僅要有值,還得符合 Email 格式;
- iscolor 后面注釋也提了,這是個別名,本質(zhì)等價于 hexcolor|rgb|rgba|hsl|hsla,屬于 validator 自帶的別名能力,符合這幾個規(guī)則任一的,我們都認(rèn)為屬于表示顏色。
- required,dive,required 這個 dive 大有來頭,注意這個 Addresses 是個 Address 數(shù)組,我們加 tag 一般只是針對單獨的數(shù)據(jù)類型,這種【容器型】的怎么辦?
這時 dive 的能力就派上用場了。
dive 的語義在于告訴 validator 不要停留在我這一級,而是繼續(xù)往下校驗,無論是 slice, array 還是 map,校驗要用的 tag 就是在 dive 之后的這個。
這樣說可能不直觀,我們來看一個例子:
[][]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
第一個 gt=0 適用于最外層的數(shù)組,出現(xiàn) dive 后,往下走,len=1
作為一個 tag 適用于內(nèi)層的 []string,此后又出現(xiàn) dive,繼續(xù)往下走,對于最內(nèi)層的每個 string,要求每個都是 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
第二個例子,看看能不能理解?
其實,只要記住,每次出現(xiàn) dive,都往里面走就 ok。
回到我們一開始的例子:
Addresses []*Address validate:"required,dive,required"
表示的意思是,我們要求 Addresses 這個數(shù)組是 required,此外對于每個元素,也得是 required。
內(nèi)置校驗器
validator 對于下面六種場景都提供了豐富的校驗器,放到 tag 里就能用。這里我們簡單看一下:
1. Fields
對于結(jié)構(gòu)體各個屬性的校驗,這里可以針對一個 field 與另一個 field 相互比較。
2. Network
網(wǎng)絡(luò)相關(guān)的格式校驗,可以用來校驗 IP 格式,TCP, UDP, URL 等
3. Strings
字符串相關(guān)的校驗,用的非常多,比如校驗是否是數(shù)字,大小寫,前后綴等,非常方便。
4. Formats
符合特定格式,如我們上面提到的 email,信用卡號,顏色,html,base64,json,經(jīng)緯度,md5 等
5. Comparisons
比較大小,用的很多
6. Other
雜項,各種通用能力,用的也非常多,我們上面用的 required 就在這一節(jié)。包括校驗是否為默認(rèn)值,最大,最小等。
7. 別名
除了上面的六個大類,還包含兩個內(nèi)部封裝的別名校驗器,我們已經(jīng)用過 iscolor,還有國家碼:
錯誤處理
Golang 的 error 是個 interface,默認(rèn)其實只提供了 Error() 這一個方法,返回一個字符串,能力比較雞肋。同樣的,validator 返回的錯誤信息也是個字符串:
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
這樣當(dāng)然不錯,但問題在于,線上環(huán)境下,很多時候我們并不是【人工地】來閱讀錯誤信息,這里的 error 最終是要轉(zhuǎn)化成錯誤信息展現(xiàn)給用戶,或者打點上報的。
我們需要有能力解析出來,是哪個結(jié)構(gòu)體的哪個屬性有問題,哪個 tag 攔截了。怎么辦?
其實 validator 返回的類型底層是 validator.ValidationErrors
,我們可以在判空之后,用它來進行類型斷言,將 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 其實一組 FieldError,所謂 FieldError 就是每一個屬性的報錯,我們的 ValidationErrors 實現(xiàn)的 func Error() string
方法,也是將各個 fieldError(對 FieldError 接口的默認(rèn)實現(xiàn))連接起來,最后 TrimSpace 清掉空格展示。
在我們拿到了 ValidationErrors 后,可以遍歷各個 FieldError,拿到業(yè)務(wù)需要的信息,用來做日志打印/打點上報/錯誤碼對照等,這里是個 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 的用法,其實整體還是非常簡潔的,我們只需要全局維護一個 validator 實例,內(nèi)部會幫我們做好緩存。此后只需要把結(jié)構(gòu)體傳入,就可以完成校驗,并提供可以解析的錯誤。
validator 的實現(xiàn)也非常精巧,只不過內(nèi)容太多,我們今天暫時覆蓋不到,更多關(guān)于Go 校驗庫validator 的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang中值類型/指針類型的變量區(qū)別總結(jié)
golang的值類型和指針類型receiver一直是大家比較混淆的地方,下面這篇文章主要給大家總結(jié)介紹了關(guān)于golang中值類型/指針類型的變量區(qū)別的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12Golang?WorkerPool線程池并發(fā)模式示例詳解
這篇文章主要為大家介紹了Golang?WorkerPool線程池并發(fā)模式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08Go關(guān)鍵字defer的使用和底層實現(xiàn)
defer是Go語言的關(guān)鍵字,一般用于資源的釋放和異常的捕捉,defer語句后將其后面跟隨的語句進行延遲處理,就是說在函數(shù)執(zhí)行完畢后再執(zhí)行調(diào)用,也就是return的ret指令之前,本文給大家介紹了Go關(guān)鍵字defer的使用和底層實現(xiàn),需要的朋友可以參考下2023-11-11Golang 使用Map實現(xiàn)去重與set的功能操作
這篇文章主要介紹了Golang 使用 Map 實現(xiàn)去重與 set 的功能操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04Golang實現(xiàn)文件夾的創(chuàng)建與刪除的方法詳解
這篇文章主要介紹了如何利用Go語言實現(xiàn)對文件夾的常用操作:創(chuàng)建于刪除。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-05-05