17

Echo 系列教程 — 定制篇2:自定义 Validator,进行输入校验

 4 years ago
source link: http://blog.studygolang.com/2020/02/echo-custom-validator/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇讲 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 [email protected]

什么都没有输出,表示一切正常。如果我们提供一个非法的邮箱地址:

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

欢迎关注我的公众号:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK