Go 語言 Web 框架 Echo 系列:定製篇2 — 自定義 Validator,進行輸入校驗

2021-03-02 polarisxu

上一篇講 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) *UniversalTranslator

New 返回一個 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 怎麼和以上兩個庫集成提供 i18n

Validator 庫提供了相應的子庫,對以上兩個庫進行了封裝。比如中文的庫: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。

相關焦點

  • gin框架中使用validator若干實用技巧
    gin框架中使用validator若干實用技巧web開發中不可避免的要對請求參數進行校驗,通常我們會在代碼中定義與請求參數相對應的模型(結構體),藉助模型綁定快捷地解析請求中的參數,例如 gin 框架中的Bind和ShouldBind系列方法。
  • 「語言實踐」Go語言文檔自動化之go-swagger
    2 重要包介紹go-openapi 介紹go-openapi倉庫屬於openapi的一個go語言分支源碼實現,那麼什麼是openapi呢,其實就是OpenAPI規範,即OpenAPI Specification,簡稱OAS,是屬於Linux基金會的一個項目,主要是為了讓文檔化更方便
  • 6個最好的Go語言Web框架
    Beego: 一個Go語言下開源的,高性能Web框架 https://github.com/astaxie/beego https://beego.meBuffalo: 一個Go語言下快速Web開發框架 https://github.com/gobuffalo/buffalo
  • Golang 語言的值驗證庫 Validator 怎麼使用?
    01介紹Validator 是基於 tag(標記)實現結構體和單個欄位的值驗證庫,它包含以下功能:使用驗證 tag(標記)或自定義驗證器進行跨欄位和跨結構體驗證能夠深入 map 鍵和值進行驗證。通過在驗證之前確定接口的基礎類型來處理類型接口。處理自定義欄位類型(如 sql 驅動程序 Valuer)。別名驗證標記,它允許將多個驗證映射到單個標記,以便更輕鬆地定義結構體上的驗證。
  • SpringBoot 如何進行優雅的數據校驗
    在運行時,Bean Validation 框架本身會根據被注釋元素的類型來選擇合適的 constraint validator 對數據進行驗證。有些時候,在用戶的應用中需要一些更複雜的 constraint。Bean Validation 提供擴展 constraint 的機制。
  • wtforms數據校驗—Flask網站製作(20)
    WTForms是一個支持多個web框架的form組件,主要用於對用戶請求數據進行驗證。只看是看不懂的,實戰後立刻通體通透!wtforms擴展_2(數據校驗)form.data 取到post.request即前臺向後臺發送的數據圖中,用form的實例self.ins_departmentinfo_models通過self.ins_departmentinfo_models.data來取到前端向後端發送的數據,該數據是一個字典,由於前端和後端都是
  • go-zero 1.1.2 發布,web 和 rpc 框架
    go-zero 1.1.2 發布了。go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大並發服務端的穩定性,經受了充分的實戰檢驗。
  • Spring Validation 最佳實踐及其實現原理,參數校驗沒那麼簡單!
    引入依賴如果spring-boot版本小於2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴。還有就是嵌套集合校驗會對集合裡面的每一項都進行校驗,例如List<Job>欄位會對這個list裡面的每一個Job對象都進行校驗集合校驗如果請求體直接傳遞了json數組給後臺,並希望對數組中的每一項都進行參數校驗。此時,如果我們直接使用java.util.Collection下的list或者set來接收數據,參數校驗並不會生效!
  • vue+element-ui實現表格列校驗
    昨天有位猿友在群裡提出校驗這麼一個問題,時隔24小時,於是我就又做了回老好人,經過我最後的引導成功讓它提前2天下班了,這個時候我才意識到,由於工作太忙
  • Go 指南:頂級 Go 框架、IDE 和工具列表
    Echo 是一個輕量級框架,支持自動 TLS、極強擴展性、支持 HTTP/2、數據綁定和校驗、模板、定製化和中間件,優化過的路由,性能和 httprouter 不相上下。值得試用。Golang 的集成開發環境(IDE)Golang 的 IDE 隨著 Go 語言的普及越來越受大家的歡迎。
  • Go-Spring 迄今最穩定版本發布了!
    詳細的更新內容如下:go-spring-parent1. SpringError 模塊添加 PanicImmediately 函數,增加錯誤碼的默認值,優化 RPC 結果的顯示;2. SpringLogger 模塊添加 Print 和 Printf 函數,可適配更多 Logger 接口,線程安全的修改 Logger 的列印級別;3.
  • 如何在Web應用中實現Velocity 與Struts 框架相互集成的應用實例
    (2)VelocityStruts 組件Velocity Tools 子項目中的 VelocityStruts 組件包含集成 Velocity 模板引擎與 Struts 應用框架的所有功能。模板引擎中的模板解析引擎對其進行處理——也就是讓Struts應用框架的forward 最終轉向某個*.vm文件。
  • Go 語言極速 web 框架 IRIS V4.1.1 發布 - OSCHINA - 中文開源...
    Go 語言極速 web 框架 IRIS V4.1.1 發布了,更新如下:4.0.0 -> 4.1.1NEW
  • 快速轉型golang(go語言)web開發 01系列概覽
    為什麼要出這個快速轉型go語言的系列?因為現在go語言在國內實在是太火了……火,就意味著有錢途^_^(是的你沒看錯,就是你想的那個錢途)Go在國內到底有多火?現在市面上的大廠:華為、阿里巴巴、騰訊、百度、拼多多、京東、字節跳動、小米、美團、滴滴、360……已經沒有不用go語言的了……但是……go火爆速度遠大於市場上go工程師的供給速度,面對市面上大量go語言的崗位需求和明朗的就業前景,必定會有很多朋友有快速轉型的需求……而且……現在市面上好多go語言的視頻教程都是時長動不動就幾十個小時
  • 想要變身Go運維開發架構師,看這一篇就夠了
    前二篇文章詳細說明了:GO核心編程以及資料庫處理的自學資料本文著重講述:Go語言Web框架開發及開源項目,學習路線、學習教程和視頻將會整理在後續一篇文章裡。深入理解pro metheus 原理 ,學習使用consul進行服務發現及管理 、Exporter開發及Alertmanagerwebhook開發。
  • 三大最棒的開源Web開發模板或框架
    如今,很少有程式設計師從頭開始設計web網頁了。大多數程式設計師都使用預製模板設計,自定義適合其內容管理系統的選擇。即使是構建複雜Web應用程式的程式設計師也依賴於模板庫。  但是,如果要為內容管理系統或靜態站點生成器構建新模板,該怎麼辦?如果想使用單個目標網頁或少量不太可能經常更改的靜態網頁構建簡單網站,該怎麼辦?
  • Spring Boot集成validation用於優雅的校驗API參數的合法性
    validation主要是校驗用戶提交的數據的合法性,比如是否為空,密碼是否符合規則,郵箱格式是否正確等等,校驗框架比較多,用的比較多的是hibernate-validator, 也支持國際化,也可以自定義校驗類型的註解,這裡只是簡單的演示校驗框架在Spring Boot中的簡單集成
  • Go 語言極速 web 框架 IRIS V4 發布
    Golang目前已經發展成為非常廣泛使用的開發語言,如果你開發WEB、後臺服務、API,都可以用到golang.
  • Linux——Shell腳本中自定義變量的應用(基礎)2
    Shell腳本的應用2(基礎)簡介:這篇文檔為shell腳本變量的應用,詳細講解了自定義變量上篇文檔的連結地址:Linux——Shell腳本的應用1(基礎),也是shell腳本的基礎內容,可以通過連結地址查看。
  • 一次使用 Go 語言編寫腳本的經歷
    本文介紹了我如何嘗試使用 Go 語言進行腳本編程的經歷。文中我將討論 Go 腳本的必要性,我們預期的表現以及可能的實現方式。在討論過程中,我講深入探討腳本、Shell 和 Shebang。最終,我們將會討論讓 Go 腳本工作的解決方案。為什麼 Go 語言適合編寫腳本?