<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
今天繼續我們的 Golang 經典開源庫學習之旅,這篇文章的主角是 validator,Golang 中經典的校驗庫,它可以讓開發者可以很便捷地通過 tag 來控制對結構體欄位的校驗,使用面非常廣泛。
本來打算一節收尾,越寫越發現 validator 整體複雜度還是很高的,而且支援了很多場景。可拆解的思路很多,於是打算分成兩篇文章來講。這篇我們會先來了解 validator 的用法,下一篇我們會關注實現的思路和原始碼解析。
Package validator implements value validations for structs and individual fields based on tags.
validator 是一個結構體引數驗證器。
它提供了【基於 tag 對結構體以及單獨屬性的校驗能力】。經典的 gin 框架就是用了 validator 作為預設的校驗器。它的能力能夠幫助開發者最大程度地減少【基礎校驗】的程式碼,你只需要一個 tag 就能完成校驗。完整的檔案參照 這裡。
目前 validator 最新版本已經升級到了 v10,我們可以用
go get github.com/go-playground/validator/v10
新增依賴後,import 進來即可
import "github.com/go-playground/validator/v10"
我們先來看一個簡單的例子,瞭解 validator 能怎樣幫助開發者完成校驗。
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] 這個區間內,並且希望 Age 這個數位在 [1, 100] 區間內。就可以用上面這個 tag。
校驗的時候只需要三步:
validator.New()
初始化一個校驗器;Struct
方法中;上面的例子中,lidajun 長度符合預期,18 這個 Age 也在區間內,預期 err 為 nil。而第二個用例 Name 和 Age 都在區間外。我們執行一下看看結果:
<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 }
乍一看好像區別不大,其實一旦結構體屬性變多,校驗規則變複雜,這個校驗函數的代價立刻會上升,另外你還要顯示的處理報錯資訊,以達到上面這樣清晰的效果(這個手寫的範例程式碼只返回了一個 bool,不好判斷是哪個沒過)。
越是大結構體,越是規則複雜,validator 的收益就越高。我們還可以把 validator 放到中介軟體裡面,對所有請求加上校驗,用的越多,效果越明顯。
其實筆者個人使用經驗來看,validator 帶來的另外兩個好處在於:
這兩個點雖然比較【意識流】,但在開發習慣上還是很重要的。
好了,到目前只是淺嘗輒止,下面我們結合範例看看 validator 到底提供了哪些能力。
我們上一節舉的例子就是最簡單的場景,在一個 struct 中定義好 validate:"xxx"
tag,然後呼叫校驗器的 err := validate.Struct(user)
方法來校驗。
這一節我們結合範例來看看最常用的場景下,我們會怎樣用 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 }
仔細觀察你會發現,第一步永遠是建立一個校驗器,一個 validator.New()
解決問題,後續一定要複用,內部有快取機制,效率比較高。
關鍵在第二步,大體上分為兩類:
err := validate.Struct(user)
來校驗;errs := validate.Var(myEmail, "required,email")
結構體校驗這個相信看完這個範例,大家已經很熟悉了。
變數校驗這裡很有意思,用起來確實簡單,大家看 validateVariable
這個範例就 ok,但是,但是,我只有一個變數,我為啥還要用這個 validator 啊?
原因很簡單,不要以為 validator 只能幹一些及其簡單的,比大小,比長度,判空邏輯。這些非常基礎的校驗用一個 if 語句也搞定。
validator 支援的校驗規則遠比這些豐富的多。
我們先把前面範例的結構體拿出來,看看支援哪些 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:
這時 dive 的能力就派上用場了。
dive 的語意在於告訴 validator 不要停留在我這一級,而是繼續往下校驗,無論是 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,繼續往下走,對於最內層的每個 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 裡就能用。這裡我們簡單看一下:
對於結構體各個屬性的校驗,這裡可以針對一個 field 與另一個 field 相互比較。
網路相關的格式校驗,可以用來校驗 IP 格式,TCP, UDP, URL 等
字串相關的校驗,用的非常多,比如校驗是否是數位,大小寫,前字尾等,非常方便。
符合特定格式,如我們上面提到的 email,信用卡號,顏色,html,base64,json,經緯度,md5 等
比較大小,用的很多
雜項,各種通用能力,用的也非常多,我們上面用的 required 就在這一節。包括校驗是否為預設值,最大,最小等。
除了上面的六個大類,還包含兩個內部封裝的別名校驗器,我們已經用過 iscolor,還有國家碼:
Golang 的 error 是個 interface,預設其實只提供了 Error() 這一個方法,返回一個字串,能力比較雞肋。同樣的,validator 返回的錯誤資訊也是個字串:
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
這樣當然不錯,但問題在於,線上環境下,很多時候我們並不是【人工地】來閱讀錯誤資訊,這裡的 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,拿到業務需要的資訊,用來做紀錄檔列印/打點上報/錯誤碼對照等,這裡是個 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 的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45