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