内容简介:上一篇讲 Binder 时提到,参数自动绑定和校验是 Web 框架很重要的两个功能,可以极大的提升开发速度,并更好的保证数据的可靠性(服务端数据校验很重要)。本节,我们就一起看看如何自定义 Echo 的表单校验功能。不同于 Binder,Echo 并没有内置数据校验的能力,也就是没有默认的 Validator 实现。然而,你可以很方便的集成第三方的数据校验库。跟 Binder 类似,Echo 提供了一个 Validator 接口,方便将第三方数据校验库集成进来。通过这个实现这个接口,可以很方便的将任何第三方
上一篇讲 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,可以验证任何的多维字段或多层级;
- 能够深入(多维)了解 map 键和值以进行验证;
- 通过在验证之前确定其基础类型来处理接口类型;
- 处理自定义字段类型,例如 sql driver Valuer;
- 别名验证标签,允许将多个验证映射到单个标签,以便更轻松地定义结构上的验证;
- 提取自定义定义的字段名称,例如可以指定在验证时提取 JSON 名称,并将其用于结果 FieldError 中;
- 可自定义的 i18n 错误消息;
- gin Web 框架的默认验证器;
一个简单的例子
通过一个简单例子来看看如何使用该库。
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必须大于或等于1
Email为必填字段
大功告成。
将 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 。
欢迎关注我的公众号:
以上所述就是小编给大家介绍的《Echo 系列教程 — 定制篇2:自定义 Validator,进行输入校验》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- vue实战 - 车牌号校验和银行校验
- 更加灵活的参数校验,Spring-boot自定义参数校验注解
- 一坨一坨的 if/else 参数校验,终于被 Spring Boot 参数校验组件整干净了
- SpringMVC——数据校验
- gin请求数据校验
- Dojo 表单校验
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。