Golang使用標籤表示式校驗結構體欄位的有效性
一、背景
在服務的API介面層面,我們常常需要驗證引數的有效性。 Golang中,大部分引數校驗場景實際上是先將資料Bind到結構體,然後校驗其欄位值。
一般地,校驗結構體欄位值有如下兩種實現方式。
- Case-By-Case 針對每個需校驗的結構體欄位分別寫校驗程式碼
- 優點:自由靈活,適應所有場景
- 缺點:重複且瑣碎的碼農工作,易使人厭煩
- 規則匹配,在結構體標籤中設定預先支援的驗證規則,如
email
、max:100
等形式- 優點:使用簡單,不需要寫瑣碎的程式碼
- 缺點:強依賴有限的規則,缺乏靈活性,無法滿足複雜場景,如多欄位關聯驗證等
思考:有沒有一種方式,即簡單易用(少寫程式碼),又能滿足各種複雜的校驗場景?
答案是:有!結構體標籤表示式 go-tagexpr 的出現,為我們提供了兼得魚和熊掌的第三種選擇。
二、認識 go-tagexpr
go-tagexpr 允許Gopher們在 struct tag 寫表示式程式碼,並通過高效能的直譯器計算其結果。
安裝
go get -u github.com/bytedance/go-tagexpr
下面使用一個小示例,演示含有列舉、比較、欄位關聯的較複雜場景。
示例程式碼
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',$)
的值。這是一個呼叫內部函式的表示式,它列印並返回字串,結果為“Uncomfortable temperature: 40”r.Eval("Temperature@alarm")
獲取更多關於 go-expr 結構體標籤表示式的語法知識 -> 檢視這裡
二、使用Validator校驗
Validator 是有 go-expr 包提供的一個採用結構體標籤表示式的引數校驗元件。
主要特性
- 它要求在每個待校驗欄位上新增結果為布林值的匿名錶達式
- 當表示式結果為false時,表示驗證不通過,此時元件將返回與該欄位相關的錯誤資訊
- 它支援使用名稱為
msg
且結果為字串的表示式作為錯誤資訊 - 允許使用者按需求自由修改錯誤資訊的模板
- 支援各種常見的運算子
- 支援訪問陣列,切片,字典成員
- 支援訪問當前結構體中的任何欄位
- 支援訪問巢狀欄位,非匯出欄位等
- 支援註冊自定義的驗證函式表示式
- 內建len,sprintf,regexp,email,phone等函式表示式
安裝
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: "[email protected]",
}
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: "[email protected]", }
-
校驗例項 m 的各欄位值是否有效,如果無效,則返回error資訊
err := vd.Validate(m)
註冊自己的校驗函式
可能你已注意到 email($)
這個表示式,它是預設註冊的一個函式表示式,用於驗證郵箱的有效性。其實我們也可以定義自己通用的函式表示式,以便較少標籤中的程式碼量,增加程式碼複用性。
下面以 email 函式的實現為例,演示如何註冊自己的校驗函式:
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可選引數,表示是否強制覆蓋已經註冊了的同名函式。
**結論:**validator的使用方法非常簡單、靈活且具有良好的擴充套件性,能夠輕鬆滿足各種複雜的驗證場景。