原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-04-validator.html
社區(qū)里曾經(jīng)有人用 圖 5-10 來嘲笑 PHP:
圖 5-10 validator 流程
這其實是一個語言無關(guān)的場景,需要進(jìn)行字段校驗的情況有很多,Web 系統(tǒng)的 Form 或 JSON 提交只是一個典型的例子。我們用 Go 來寫一個類似上圖的校驗示例。然后研究怎么一步步對其進(jìn)行改進(jìn)。
假設(shè)我們的數(shù)據(jù)已經(jīng)通過某個開源綁定庫綁定到了具體的結(jié)構(gòu)體上。
type RegisterReq struct {
Username string `json:"username"`
PasswordNew string `json:"password_new"`
PasswordRepeat string `json:"password_repeat"`
Email string `json:"email"`
}
func register(req RegisterReq) error{
if len(req.Username) > 0 {
if len(req.PasswordNew) > 0 && len(req.PasswordRepeat) > 0 {
if req.PasswordNew == req.PasswordRepeat {
if emailFormatValid(req.Email) {
createUser()
return nil
} else {
return errors.New("invalid email")
}
} else {
return errors.New("password and reinput must be the same")
}
} else {
return errors.New("password and password reinput must be longer than 0")
}
} else {
return errors.New("length of username cannot be 0")
}
}
我們用 Go 里成功寫出了波動拳開路的箭頭型代碼。。這種代碼一般怎么進(jìn)行優(yōu)化呢?
很簡單,在《重構(gòu)》一書中已經(jīng)給出了方案:Guard Clauses。
func register(req RegisterReq) error{
if len(req.Username) == 0 {
return errors.New("length of username cannot be 0")
}
if len(req.PasswordNew) == 0 || len(req.PasswordRepeat) == 0 {
return errors.New("password and password reinput must be longer than 0")
}
if req.PasswordNew != req.PasswordRepeat {
return errors.New("password and reinput must be the same")
}
if emailFormatValid(req.Email) {
return errors.New("invalid email")
}
createUser()
return nil
}
代碼更清爽,看起來也不那么別扭了。這是比較通用的重構(gòu)理念。雖然使用了重構(gòu)方法使我們的校驗過程代碼看起來優(yōu)雅了,但我們還是得為每一個 http
請求都去寫這么一套差不多的 validate()
函數(shù),有沒有更好的辦法來幫助我們解除這項體力勞動?答案就是 validator。
從設(shè)計的角度講,我們一定會為每個請求都聲明一個結(jié)構(gòu)體。前文中提到的校驗場景我們都可以通過 validator 完成工作。還以前文中的結(jié)構(gòu)體為例。為了美觀起見,我們先把 json tag 省略掉。
這里我們引入一個新的 validator 庫:
https://github.com/go-playground/validator
使用 go get github.com/go-playground/validator/v10
可以下載 validator 庫。
import "github.com/go-playground/validator/v10"
type RegisterReq struct {
// 字符串的 gt=0 表示長度必須 > 0,gt = greater than
Username string `validate:"gt=0"`
// 同上
PasswordNew string `validate:"gt=0"`
// eqfield 跨字段相等校驗
PasswordRepeat string `validate:"eqfield=PasswordNew"`
// 合法 email 格式校驗
Email string `validate:"email"`
}
var validate = validator.New()
func validateFunc(req RegisterReq) error {
err := validate.Struct(req)
if err != nil {
doSomething()
return err
}
...
}
這樣就不需要在每個請求進(jìn)入業(yè)務(wù)邏輯之前都寫重復(fù)的 validate()
函數(shù)了。本例中只列出了這個校驗器非常簡單的幾個功能。
我們試著跑一下這個程序,輸入?yún)?shù)設(shè)置為:
//...
var req = RegisterReq {
Username : "Xargin",
PasswordNew : "ohno",
PasswordRepeat : "ohn",
Email : "alex@abc.com",
}
err := validateFunc(req)
fmt.Println(err)
// Key: 'RegisterReq.PasswordRepeat' Error:Field validation for
// 'PasswordRepeat' failed on the 'eqfield' tag
如果覺得這個 validator
提供的錯誤信息不夠人性化,例如要把錯誤信息返回給用戶,那就不應(yīng)該直接顯示英文了??梢葬槍γ糠N tag 進(jìn)行錯誤信息定制,讀者可以自行探索。
從結(jié)構(gòu)上來看,每一個結(jié)構(gòu)體都可以看成是一棵樹。假如我們有如下定義的結(jié)構(gòu)體:
type Nested struct {
Email string `validate:"email"`
}
type T struct {
Age int `validate:"eq=10"`
Nested Nested
}
把這個結(jié)構(gòu)體畫成一棵樹,見 圖 5-11:
圖 5-11 validator 樹
從字段校驗的需求來講,無論我們采用深度優(yōu)先搜索還是廣度優(yōu)先搜索來對這棵結(jié)構(gòu)體樹來進(jìn)行遍歷,都是可以的。
我們來寫一個遞歸的深度優(yōu)先搜索方式的遍歷示例:
package main
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
type Nested struct {
Email string `validate:"email"`
}
type T struct {
Age int `validate:"eq=10"`
Nested Nested
}
func validateEmail(input string) bool {
if pass, _ := regexp.MatchString(
`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4}), input,
); pass {
return true
}
return false
}
func validate(v interface{}) (bool, string) {
validateResult := true
errmsg := "success"
vt := reflect.TypeOf(v)
vv := reflect.ValueOf(v)
for i := 0; i <vv.NumField(); i++ {
fieldVal := vv.Field(i)
tagContent := vt.Field(i).Tag.Get("validate")
k := fieldVal.Kind()
switch k {
case reflect.Int:
val := fieldVal.Int()
tagValStr := strings.Split(tagContent, "=")
tagVal, _ := strconv.ParseInt(tagValStr[1], 10, 64)
if val != tagVal {
errmsg = "validate int failed, tag is:"+ strconv.FormatInt(
tagVal, 10,
)
validateResult = false
}
case reflect.String:
val := fieldVal.String()
tagValStr := tagContent
switch tagValStr {
case "email":
nestedResult := validateEmail(val)
if nestedResult == false {
errmsg = "validate mail failed, field val is:"+ val
validateResult = false
}
}
case reflect.Struct:
// 如果有內(nèi)嵌的 struct,那么深度優(yōu)先遍歷
// 就是一個遞歸過程
valInter := fieldVal.Interface()
nestedResult, msg := validate(valInter)
if nestedResult == false {
validateResult = false
errmsg = msg
}
}
}
return validateResult, errmsg
}
func main() {
var a = T{Age: 10, Nested: Nested{Email: "abc@abc.com"}}
validateResult, errmsg := validate(a)
fmt.Println(validateResult, errmsg)
}
這里我們簡單地對 eq=x
和 email
這兩個 tag 進(jìn)行了支持,讀者可以對這個程序進(jìn)行簡單的修改以查看具體的 validate 效果。為了演示精簡掉了錯誤處理和復(fù)雜情況的處理,例如 reflect.Int8/16/32/64
,reflect.Ptr
等類型的處理,如果給生產(chǎn)環(huán)境編寫校驗庫的話,請務(wù)必做好功能的完善和容錯。
在前一小節(jié)中介紹的開源校驗組件在功能上要遠(yuǎn)比我們這里的例子復(fù)雜的多。但原理很簡單,就是用反射對結(jié)構(gòu)體進(jìn)行樹形遍歷。有心的讀者這時候可能會產(chǎn)生一個問題,我們對結(jié)構(gòu)體進(jìn)行校驗時大量使用了反射,而 Go 的反射在性能上不太出眾,有時甚至?xí)绊懙轿覀兂绦虻男阅堋_@樣的考慮確實有一些道理,但需要對結(jié)構(gòu)體進(jìn)行大量校驗的場景往往出現(xiàn)在 Web 服務(wù),這里并不一定是程序的性能瓶頸所在,實際的效果還是要從 pprof 中做更精確的判斷。
如果基于反射的校驗真的成為了你服務(wù)的性能瓶頸怎么辦?現(xiàn)在也有一種思路可以避免反射:使用 Go 內(nèi)置的 Parser 對源代碼進(jìn)行掃描,然后根據(jù)結(jié)構(gòu)體的定義生成校驗代碼。我們可以將所有需要校驗的結(jié)構(gòu)體放在單獨(dú)的包內(nèi)。這就交給讀者自己去探索了。
![]() | ![]() |
更多建議: