解析 Golang 經典校驗庫 validator 用法
https://juejin.cn/post/7135803728916905997
開篇
今天繼續我們的 Golang 經典開源庫學習之旅,這篇文章的主角是 validator,Golang 中經典的校驗庫,它可以讓開發者可以很便捷地通過 tag 來控制對結構體字段的校驗,使用面非常廣泛。
validator
Package validator implements value validations for structs and individual fields based on tags.
validator[1] 是一個結構體參數驗證器。
它提供了【基於 tag 對結構體以及單獨屬性的校驗能力】。經典的 gin[2] 框架就是用了 validator 作爲默認的校驗器。它的能力能夠幫助開發者最大程度地減少【基礎校驗】的代碼,你只需要一個 tag 就能完成校驗。完整的文檔參照 這裏 [3]。
目前 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
方法中; -
校驗返回的 error 是否爲 nil 即可。
上面的例子中,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 帶來的另外兩個好處在於:
-
因爲需要經常使用校驗能力,養成了習慣,每定義一個結構,都事先想好每個屬性應該有哪些約束,促使開發者思考自己的模型。這一點非常重要,很多時候我們就是太隨意定義一些結構,沒有對應的校驗,結果導致各種髒數據,把校驗邏輯一路下沉;
-
有了 tag 來描述約束規則,讓結構體本身更容易理解,可讀性,可維護性提高。一看結構體,掃幾眼 tag 就知道業務對它的預期。
這兩個點雖然比較【意識流】,但在開發習慣上還是很重要的。
好了,到目前只是淺嘗輒止,下面我們結合示例看看 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:
-
required 要求必須有值,不爲空;
-
gte=0,lte=130 其中 gte 代表大於等於,lte 代表小於等於,這個語義是 [0,130] 區間;
-
required, emal 不僅僅要有值,還得符合 Email 格式;
-
iscolor 後面註釋也提了,這是個別名,本質等價於 hexcolor|rgb|rgba|hsl|hsla,屬於 validator 自帶的別名能力,符合這幾個規則任一的,我們都認爲屬於表示顏色。
-
required,dive,required 這個 dive 大有來頭,注意這個 Addresses 是個 Address 數組,我們加 tag 一般只是針對單獨的數據類型,這種【容器型】的怎麼辦?
這時 dive[4] 的能力就派上用場了。
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 裏就能用。這裏我們簡單看一下:
(注:想看完整的建議參考文檔 [5] 以及倉庫 README[6])
1. Fields
對於結構體各個屬性的校驗,這裏可以針對一個 field 與另一個 field 相互比較。
2. Network
網絡相關的格式校驗,可以用來校驗 IP 格式,TCP, UDP, URL 等
3. Strings
字符串相關的校驗,用的非常多,比如校驗是否是數字,大小寫,前後綴等,非常方便。
4. Formats
符合特定格式,如我們上面提到的 email,信用卡號,顏色,html,base64,json,經緯度,md5 等
5. Comparisons
比較大小,用的很多
6. Other
雜項,各種通用能力,用的也非常多,我們上面用的 required 就在這一節。包括校驗是否爲默認值,最大,最小等。
7. 別名
除了上面的六個大類,還包含兩個內部封裝的別名校驗器,我們已經用過 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 實例,內部會幫我們做好緩存。此後只需要把結構體傳入,就可以完成校驗,並提供可以解析的錯誤。
參考資料
[1]
validator: https://github.com/go-playground/validator
[2]
gin: https://github.com/gin-gonic/gin
[3]
這裏: https://pkg.go.dev/github.com/go-playground/validator/v10
[4]
dive: https://pkg.go.dev/github.com/go-playground/validator/v10#hdr-Dive
[5]
文檔: https://pkg.go.dev/github.com/go-playground/validator/v10#hdr-Baked_In_Validators_and_Tags
[6]
README: https://github.com/go-playground/validator
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5Pvf5_hHBqKaHxYtjjAEjg