一、背景
在服務的API接口層面本慕,我們常常需要驗證參數(shù)的有效性。
Golang中侧漓,大部分參數(shù)校驗場景實際上是先將數(shù)據(jù)Bind到結構體锅尘,然后校驗其字段值。
一般地布蔗,校驗結構體字段值有如下兩種實現(xiàn)方式藤违。
- Case-By-Case 針對每個需校驗的結構體字段分別寫校驗代碼
- 優(yōu)點:自由靈活,適應所有場景
- 缺點:重復且瑣碎的碼農(nóng)工作纵揍,易使人厭煩
- 規(guī)則匹配顿乒,在結構體標簽中設置預先支持的驗證規(guī)則,如
email
泽谨、max:100
等形式- 優(yōu)點:使用簡單璧榄,不需要寫瑣碎的代碼
- 缺點:強依賴有限的規(guī)則,缺乏靈活性吧雹,無法滿足復雜場景骨杂,如多字段關聯(lián)驗證等
思考:有沒有一種方式,即簡單易用(少寫代碼)雄卷,又能滿足各種復雜的校驗場景腊脱?
答案是:有!結構體標簽表達式 go-tagexpr 的出現(xiàn)龙亲,為我們提供了兼得魚和熊掌的第三種選擇陕凹。
二、認識 go-tagexpr
go-tagexpr 允許Gopher們在 struct tag 寫表達式代碼鳄炉,并通過高性能的解釋器計算其結果杜耙。
安裝
go get -u github.com/bytedance/go-tagexpr
下面使用一個小示例,演示含有枚舉拂盯、比較佑女、字段關聯(lián)的較復雜場景。
示例代碼
import (
"fmt"
tagexpr "github.com/bytedance/go-tagexpr"
)
func ExampleTagexpr() {
vm := tagexpr.New("te")
type Meteorology struct {
Season string `te:"$=='spring'||$=='summer'||$=='autumn'||$=='winter'"`
Weather string `te:"$!='snowing' || (Season)$=='winter'"`
Temperature int `te:"{range:$>=-10 && $<38}{alarm:sprintf('Uncomfortable temperature: %v',$)}"`
}
m := &Meteorology{
Season: "summer",
Weather: "snowing",
Temperature: 40,
}
r := vm.MustRun(m)
fmt.Println(r.Eval("Season"))
fmt.Println(r.Eval("Weather"))
fmt.Println(r.Eval("Temperature@range"))
fmt.Println(r.Eval("Temperature@alarm"))
// Output:
// true
// false
// false
// Uncomfortable temperature: 40
}
代碼詮釋:
-
新建一個標簽名稱為 te 的解釋器
vm := tagexpr.New("te")
-
定義一個結構體谈竿,添加標簽表達式团驱,并實例化一個 m 對象。其中
$
表示當前字段值空凸,(Season)$
表示 Season 字段的值type Meteorology struct { Season string `te:"$=='spring'||$=='summer'||$=='autumn'||$=='winter'"` Weather string `te:"$!='snowing' || (Season)$=='winter'"` Temperature int `te:"{range:$>=-10 && $<38}{alarm:sprintf('Uncomfortable temperature: %v',$)}"` } m := &Meteorology{ Season: "summer", Weather: "snowing", Temperature: 40, }
-
將對象實例 m 放入解釋器中運行嚎花,返回表達式對象 r
r := vm.MustRun(m)
-
計算 Season 字段匿名表達式(
$=='spring'||$=='summer'||$=='autumn'||$=='winter'
)的值。因字段值 summer 在窮舉列表中呀洲,故表達式結果為“true”r.Eval("Season")
-
計算 Weather 字段匿名表達式
$!='snowing' || (Season)$=='winter'
的值紊选。因字段值為 snowing 且 Season 為 summer,故表達式結果為“false”r.Eval("Weather")
-
計算 Temperature 字段的
range
表達式$>=-10 && $<38
的值道逗。因字段值為 40兵罢,超出給出的范圍,所以結果為“false”r.Eval("Temperature@range")
-
計算 Temperature 字段的
alarm
表達式sprintf('Uncomfortable temperature: %v',$)
的值滓窍。這是一個調(diào)用內(nèi)部函數(shù)的表達式卖词,它打印并返回字符串,結果為“Uncomfortable temperature: 40”r.Eval("Temperature@alarm")
獲取更多關于 go-expr 結構體標簽表達式的語法知識 -> 查看這里
二吏夯、使用Validator校驗
Validator 是有 go-expr 包提供的一個采用結構體標簽表達式的參數(shù)校驗組件此蜈。
主要特性
- 它要求在每個待校驗字段上添加結果為布爾值的匿名表達式
- 當表達式結果為false時,表示驗證不通過锦亦,此時組件將返回與該字段相關的錯誤信息
- 它支持使用名稱為
msg
且結果為字符串的表達式作為錯誤信息 - 允許用戶按需求自由修改錯誤信息的模板
- 支持各種常見的運算符
- 支持訪問數(shù)組舶替,切片,字典成員
- 支持訪問當前結構體中的任何字段
- 支持訪問嵌套字段杠园,非導出字段等
- 支持注冊自定義的驗證函數(shù)表達式
- 內(nèi)置len顾瞪,sprintf,regexp抛蚁,email陈醒,phone等函數(shù)表達式
安裝
go get -u github.com/bytedance/go-tagexpr
我們基于前面示例稍作修改,來演示如何使用validator校驗結構體字段的有效性瞧甩。
示例代碼
import (
"fmt"
"github.com/bytedance/go-tagexpr/validator"
)
func ExampleValidator() {
vd := validator.New("vd")
type Meteorology struct {
Season string `vd:"$=='spring'||$=='summer'||$=='autumn'||$=='winter'"`
Weather string `vd:"$!='snowing' || (Season)$=='winter'"`
Temperature int `vd:"{@:$>=-10 && $<38}{msg:sprintf('Uncomfortable temperature: %v',$)}"`
Contact string `vd:"email($)"`
}
m := &Meteorology{
Season: "summer",
Weather: "rain",
Temperature: 40,
Contact: "henrylee2cn@gmail.com",
}
err := vd.Validate(m)
if err != nil {
fmt.Println(err)
}
// Output:
// Uncomfortable temperature: 40
}
代碼詮釋:
-
新建一個標簽名稱為 vd 的校驗器
vd := validator.New("vd")
-
定義一個結構體钉跷,在標簽上添加校驗表達式,并使用 m 實例進行測試肚逸。
type Meteorology struct { Season string `vd:"$=='spring'||$=='summer'||$=='autumn'||$=='winter'"` Weather string `vd:"$!='snowing' || (Season)$=='winter'"` Temperature int `vd:"{@:$>=-10 && $<38}{msg:sprintf('Uncomfortable temperature: %v',$)}"` Contact string `vd:"email($)"` } m := &Meteorology{ Season: "summer", Weather: "rain", Temperature: 40, Contact: "henrylee2cn@gmail.com", }
-
校驗實例 m 的各字段值是否有效爷辙,如果無效彬坏,則返回error信息
err := vd.Validate(m)
注冊自己的校驗函數(shù)
可能你已注意到 email($)
這個表達式,它是默認注冊的一個函數(shù)表達式膝晾,用于驗證郵箱的有效性栓始。其實我們也可以定義自己通用的函數(shù)表達式,以便較少標簽中的代碼量血当,增加代碼復用性幻赚。
下面以 email 函數(shù)的實現(xiàn)為例,演示如何注冊自己的校驗函數(shù):
var pattern = "^([A-Za-z0-9_\\-\\.\u4e00-\u9fa5])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,8})$"
emailRegexp := regexp.MustCompile(pattern)
validator.RegValidateFunc("email", func(args ...interface{}) bool {
if len(args) != 1 {
return false
}
s, ok := args[0].(string)
if !ok {
return false
}
return emailRegexp.MatchString(s)
}, true)
其中臊旭,validator.RegValidateFunc 的定義如下:
func RegValidateFunc(funcName string, fn func(args ...interface{}) bool, force ...bool) error
RegValidateFunc的force可選參數(shù)落恼,表示是否強制覆蓋已經(jīng)注冊了的同名函數(shù)。
結論:validator的使用方法非常簡單离熏、靈活且具有良好的擴展性佳谦,能夠輕松滿足各種復雜的驗證場景。
獲取更多關于 validator 校驗器的語法知識 -> 查看這里