上一篇講 Binder 時提到,參數自動綁定和校驗是 Web 框架很重要的兩個功能,可以極大的提升開發速度,並更好的保證數據的可靠性(服務端數據校驗很重要)。本節,我們就一起看看如何自定義 Echo 的表單校驗功能。
不同於 Binder,Echo 並沒有內置數據校驗的能力,也就是沒有默認的 Validator 實現。然而,你可以很方便的集成第三方的數據校驗庫。跟 Binder 類似,Echo 提供了一個 Validator 接口,方便將第三方數據校驗庫集成進來。
Validator interface {
Validate(i interface{}) error
}通過這個實現這個接口,可以很方便的將任何第三方數據校驗庫集成到 Echo 中。在 Awesome-Go 上可以找到第三方數據校驗庫:https://github.com/avelino/awesome-go#validation。本文我們使用最流行的 https://github.com/go-playground/validator 庫來講解。
go-playground/validator這是一個 Go 結構體及欄位校驗器,包括:跨欄位和跨結構體校驗,Map,切片和數組,是目前校驗器相關庫中 Star 數最高的一個,對國際化支持也很好,建議大家使用它。
它具有以下獨特功能:
通過使用驗證標籤(tag)或自定義驗證程序進行跨欄位和跨結構體驗證;切片,數組和 map,可以驗證任何的多維欄位或多層級;處理自定義欄位類型,例如 sql driver Valuer;別名驗證標籤,允許將多個驗證映射到單個標籤,以便更輕鬆地定義結構上的驗證;提取自定義定義的欄位名稱,例如可以指定在驗證時提取 JSON 名稱,並將其用於結果 FieldError 中;一個簡單的例子通過一個簡單例子來看看如何使用該庫。
package main
import (
"fmt"
"flag"
"github.com/go-playground/validator/v10"
)
type User struct {
Name string `validate:"required"`
Age uint `validate:"gte=1,lte=130"`
Email string `validate:"required,email"`
}
var (
name string
age uint
email string
)
func init() {
flag.StringVar(&name, "name", "", "輸入名字")
flag.UintVar(&age, "age", 0, "輸入年齡")
flag.StringVar(&email, "email", "", "輸入郵箱")
}
func main() {
flag.Parse()
user := &User{
Name: name,
Age: age,
Email: email,
}
validate := validator.New()
err := validate.Struct(user)
if err != nil {
fmt.Println(err)
}
}執行如下命令,運行代碼:
go run main.go -name studygolang -age 7 -email polaris@studygolang.com什麼都沒有輸出,表示一切正常。如果我們提供一個非法的郵箱地址:
go run main.go -name studygolang -age 7 -email polaris@studygolang輸出如下錯誤:
Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag錯誤顯示不友好。怎麼能夠更友好,並進行國際化呢?
國際化(i18n)在介紹校驗庫錯誤消息國際化之前,有一個概念需要了解下,那就是 CLDR。
什麼是 CLDR?它是 i18n 的一套核心規範( Common Locale Data Respository),即通用的本地化數據存儲庫,什麼意思呢?比如我們的手機,電腦都可以選擇語言模式為 英語、漢語、日語、法語等等,這套操作背後的規範,就是 CLDR;CLDR 是以 Unicode 的編碼標準作為前提,將多國的語言文字進行編碼的。
看看官方對於 CLDR 的說明,官方網址:http://cldr.unicode.org/
Unicode CLDR 提供了支持世界語言的軟體的關鍵構建塊,並且具有最大和最廣泛的本地設置數據標準存儲庫。大量的公司使用此數據進行軟體的國際化和本地化,使它們的軟體適應此類通用軟體任務的不同語言的約定。
需要進行國際化和本地化的主要包括:
用于格式化和解析的特定於語言環境的模式:日期,時間,時區,數字和貨幣值,度量單位,…名稱的翻譯:語言,腳本,國家和地區,貨幣,時代,月份,工作日,白天,時區,城市和時間單位,表情符號字符和序列(和搜索關鍵字),…語言和文字信息:使用的字符;複數情況;性別;大寫;分類和搜索規則;寫作方向;音譯規則;拼寫數字的規則;將文本分割成字符,單詞和句子的規則;鍵盤布局…國家/地區信息:語言使用情況,貨幣信息,日曆首選項,星期慣例等…有效性:Unicode 語言環境,語言,腳本,區域和擴展名的定義,別名和有效性信息,…CLDR 的 Go 語言實現本文講解的校驗庫是 go-playground 這個組織創建的,它們還提供了其他的一些有用庫,其中就包括了 CLDR 的 Go 語言實現,這就是 locales。
該庫是從 CLDR 項目生成的一組語言環境,可以單獨使用或在 i18n 軟體包中使用;這些是專為 https://github.com/go-playground/universal-translator 構建的,但也可以單獨他用。
這引出了該組織的另外一個庫:universal-translator。
universal-translator:一個使用 CLDR 數據+複數規則(比如英語很多複數規則是加 s)的 Go i18n 轉換器(翻譯器)。該庫是 locales 的薄包裝,以便存儲和翻譯文本,供你在應用程式中使用。
universal-translator 簡明教程這個通用的翻譯器包主要包含了兩個核心數據結構:Translator 接口和 UniversalTranslator 結構體,其他的是錯誤類型。我們先看 Translator 接口。(注意,該包的包名是 ut)
Translator 接口
type Translator interface {
locales.Translator
// adds a normal translation for a particular language/locale
// {#} is the only replacement type accepted and are ad infinitum
// eg. one: '{0} day left' other: '{0} days left'
Add(key interface{}, text string, override bool) error
// adds a cardinal plural translation for a particular language/locale
// {0} is the only replacement type accepted and only one variable is accepted as
// multiple cannot be used for a plural rule determination, unless it is a range;
// see AddRange below.
// eg. in locale 'en' one: '{0} day left' other: '{0} days left'
AddCardinal(key interface{}, text string, rule locales.PluralRule, override bool) error
// adds an ordinal plural translation for a particular language/locale
// {0} is the only replacement type accepted and only one variable is accepted as
// multiple cannot be used for a plural rule determination, unless it is a range;
// see AddRange below.
// eg. in locale 'en' one: '{0}st day of spring' other: '{0}nd day of spring'
// - 1st, 2nd, 3rd...
AddOrdinal(key interface{}, text string, rule locales.PluralRule, override bool) error
// adds a range plural translation for a particular language/locale
// {0} and {1} are the only replacement types accepted and only these are accepted.
// eg. in locale 'nl' one: '{0}-{1} day left' other: '{0}-{1} days left'
AddRange(key interface{}, text string, rule locales.PluralRule, override bool) error
// creates the translation for the locale given the 'key' and params passed in
T(key interface{}, params ...string) (string, error)
// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
// and param passed in
C(key interface{}, num float64, digits uint64, param string) (string, error)
// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
// and param passed in
O(key interface{}, num float64, digits uint64, param string) (string, error)
// creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
// 'digit2' arguments and 'param1' and 'param2' passed in
R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) (string, error)
// VerifyTranslations checks to ensures that no plural rules have been
// missed within the translations.
VerifyTranslations() error
}關於該接口需要需要如下幾點說明
內嵌了 locales.Translator 接口;幾類複數規則:cardinal plural(基數複數規則,即單數和複數兩種);ordinal plural(序數複數規則,如 1st, 2nd, 3rd…);ordinal plural (範圍複數規則,如 0-1)。對中文來說,這裡大部分不需要。幾個 Add 方法,和上面幾類規則對應;一個 key 和 一個帶站位符的 text;單字符的幾個方法和 Add 幾個方法的對應關係:T -> Add;C -> AddCardinal;O -> AddOrdinal;R -> AddRange ;表示用具體的值替換 key 表示的文本 text 中的佔位符。以上方法參數中,num 表示佔位符處的值,但對於有複數形式的語言,這個值必須符合複數語言的規範,否則會報錯;digits 表示 num 值的有效數字(或者說小數位數);VerifyTranslations 確保翻譯庫中沒有缺少對應的語言規則;UniversalTranslator 結構體
它用於保存所有語言環境和翻譯數據。該結構體方法不多,我們關注幾個核心的。
func New(fallback locales.Translator, supportedLocales ...locales.Translator) *UniversalTranslatorNew 返回一個 UniversalTranslator 實例,該實例具有後備語言環境(fallback)和應支持的語言環境(supportedLocales)。可以看到,New 函數接收的參數是 locales.Translator 類型,因此我們肯定需要用到 locales 包。
得到 UniversalTranslator 實例後,需要獲得 universal-translator 包中的 Translator 接口實例,這就用到了下面幾個方法。
1)GetTranslator
func (t *UniversalTranslator) GetTranslator(locale string) (trans Translator, found bool)返回給定語言環境的指定翻譯器,如果未找到,則返回後備語言環境的翻譯器(即 New 中的 fallback)。
2)GetFallback
func (t *UniversalTranslator) GetFallback() Translator直接返回後備語言環境的翻譯器。
3)FindTranslator
func (t *UniversalTranslator) FindTranslator(locales ...string) (trans Translator, found bool)嘗試根據語言環境數組查找翻譯器,並返回它可以找到的第一個翻譯器,否則返回後備翻譯器。
總結來說,New 函數加上這三個方法,相當於是 locales.Translator 到 ut.Translator 的轉換。
示例
通過一個實際的例子來學習下這兩個包的使用。
package main
import (
"flag"
"fmt"
"github.com/go-playground/locales"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
"github.com/go-playground/locales/zh_Hant_TW"
ut "github.com/go-playground/universal-translator"
)
var universalTraslator *ut.UniversalTranslator
func main() {
acceptLanguage := flag.String("language", "zh", "語言")
flag.Parse()
e := en.New()
universalTraslator = ut.New(e, e, zh.New(), zh_Hant_TW.New())
translator, _ := universalTraslator.GetTranslator(*acceptLanguage)
switch *acceptLanguage {
case "zh":
translator.Add("welcome", "歡迎 {0} 來到 studygolang.com!", false)
translator.AddCardinal("days", "你只剩 {0} 天時間可以註冊", locales.PluralRuleOther, false)
translator.AddOrdinal("day-of-month", "第{0}天", locales.PluralRuleOther, false)
translator.AddRange("between", "距離 {0}-{1} 天", locales.PluralRuleOther, false)
case "en":
translator.Add("welcome", "Welcome {0} to studygolang.com.", false)
translator.AddCardinal("days", "You have {0} day left to register", locales.PluralRuleOne, false)
translator.AddOrdinal("day-of-month", "{0}st", locales.PluralRuleOne, false)
translator.AddRange("between", "It's {0}-{1} days away", locales.PluralRuleOther, false)
}
fmt.Println(translator.T("welcome", "polaris"))
fmt.Println(translator.C("days", 1, 0, translator.FmtNumber(1, 0)))
fmt.Println(translator.O("day-of-month", 1, 0, translator.FmtNumber(1, 0)))
fmt.Println(translator.R("between", 1, 0, 2, 0, translator.FmtNumber(1, 0), translator.FmtNumber(2, 0)))
}主要通過這個例子說明相關函數的使用。
根據 acceptLanguage 的不同值,設置不同的語言文案;對於中文來說,沒有複數,因此 AddXX 三個方法的第二個參數都是 locales.PluralRuleOther,表示該語言環境沒有複數形式;英文環境下,PluralRule 規則不能亂填,根據實際情況來;最後在實際填充值時,num 表示佔位符要填入的值,digits 表示 num 這個值最終要保留幾位小數;FmtNumber 方法的參數需要和前面的 num 和 digits 對應上,第一個參數是 num 的值,第二個是 digits 的值;Validator 怎麼和以上兩個庫集成提供 i18nValidator 庫提供了相應的子庫,對以上兩個庫進行了封裝。比如中文的庫:github.com/go-playground/validator/translations/zh ,這些子庫提供了一個 RegisterDefaultTranslations ,為所有內置標籤的驗證器註冊一組默認翻譯。
func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (err error)具體怎麼做?還是看最開始的例子,其他不變,main 函數改為如下:
func main() {
flag.Parse()
user := &User{
Name: name,
Age: age,
Email: email,
}
validate := validator.New()
e := en.New()
uniTrans := ut.New(e, e, zh.New(), zh_Hant_TW.New())
translator, _ := uniTrans.GetTranslator("zh")
zh_translate.RegisterDefaultTranslations(validate, translator)
err := validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
for _, err := range errs {
fmt.Println(err.Translate(translator))
}
}
}註冊一個默認的中文翻譯器,在校驗出錯後,對錯誤進行翻譯。不輸入任何參數運行程序,輸出:
Name為必填欄位Age必須大於或等於1Email為必填欄位
大功告成。
將 Validator 集成到 Echo 中首先,需要定義一個類型,實現 Echo 的接口 Validator :
type CustomValidator struct {
once sync.Once
validate *validator.Validate
}
func (c *CustomValidator) Validate(i interface{}) error {
c.lazyInit()
return c.validate.Struct(i)
}
func (c *CustomValidator) lazyInit() {
c.once.Do(func() {
c.validate = validator.New()
})
}因為 validator.Validate 實例化做了不少事情,這裡將實例化推遲到使用時。簡單幾行代碼就實現了一個自定義的 Validator。
接下來和 Echo 集成起來就很容易了。
e := echo.New()
e.Validator = &CustomValidator{}之後就可以在需要進行表單校驗的地方通過 ctx.Validate() 進行校驗。
自此我們完成了 Validator 集成到 Echo 的功能。
還剩最後一塊內容,那就是校驗錯誤信息的國際化顯示。國際化相關的內容,上面有了較詳細的介紹,Validator 集成到 Echo 後如何國際化我們在後面實戰篇再講。
完整代碼見:
https://github.com/polaris1119/go-echo-example/blob/master/pkg/validator/validator.go。