上一篇讲 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 结构体及字段校验器,包括:跨字段和跨结构体校验,Map,切片和数组,是目前校验器相关库中 Star 数最高的一个,对国际化支持也很好,建议大家使用它。
它具有以下独特功能:
通过一个简单例子来看看如何使用该库。
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
错误显示不友好。怎么能够更友好,并进行国际化呢?
在介绍校验库错误消息国际化之前,有一个概念需要了解下,那就是 CLDR。
它是 i18n 的一套核心规范( Common Locale Data Respository),即通用的本地化数据存储库,什么意思呢?比如我们的手机,电脑都可以选择语言模式为 英语、汉语、日语、法语等等,这套操作背后的规范,就是 CLDR;CLDR 是以 Unicode 的编码标准作为前提,将多国的语言文字进行编码的。
看看官方对于 CLDR 的说明,官方网址:http://cldr.unicode.org/
Unicode CLDR 提供了支持世界语言的软件的关键构建块,并且具有最大和最广泛的本地设置数据标准存储库。大量的公司使用此数据进行软件的国际化和本地化,使它们的软件适应此类通用软件任务的不同语言的约定。
需要进行国际化和本地化的主要包括:
本文讲解的校验库是 go-playground 这个组织创建的,它们还提供了其他的一些有用库,其中就包括了 CLDR 的 Go 语言实现,这就是 locales 。
该库是从 CLDR 项目生成的一组语言环境,可以单独使用或在 i18n 软件包中使用;这些是专为 https://github.com/go-playground/universal-translator 构建的,但也可以单独他用。
这引出了该组织的另外一个库:universal-translator 。
universal-translator :一个使用 CLDR 数据+复数规则(比如英语很多复数规则是加 s)的 Go i18n 转换器(翻译器)。该库是locales 的薄包装,以便存储和翻译文本,供你在应用程序中使用。
这个通用的翻译器包主要包含了两个核心数据结构: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
}
关于该接口需要需要如下几点说明
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)))
}
主要通过这个例子说明相关函数的使用。
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为必填字段
大功告成。
首先,需要定义一个类型,实现 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。