1. 程式人生 > >Golang使用標籤表示式校驗結構體欄位的有效性

Golang使用標籤表示式校驗結構體欄位的有效性

開發十年,就只剩下這套架構體系了! >>>   

一、背景

在服務的API介面層面,我們常常需要驗證引數的有效性。 Golang中,大部分引數校驗場景實際上是先將資料Bind到結構體,然後校驗其欄位值。

一般地,校驗結構體欄位值有如下兩種實現方式。

  1. Case-By-Case 針對每個需校驗的結構體欄位分別寫校驗程式碼
    • 優點:自由靈活,適應所有場景
    • 缺點:重複且瑣碎的碼農工作,易使人厭煩
  2. 規則匹配,在結構體標籤中設定預先支援的驗證規則,如emailmax: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的使用方法非常簡單、靈活且具有良好的擴充套件性,能夠輕鬆滿足各種複雜的驗證場景。

獲取更多關於 validator 校驗器的語法知識 ->