惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
D
Docker
GbyAI
GbyAI
B
Blog RSS Feed
博客园 - 司徒正美
博客园 - Franky
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
C
Check Point Blog
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
S
Secure Thoughts
博客园 - 聂微东
L
LINUX DO - 最新话题
U
Unit 42
SecWiki News
SecWiki News
A
Arctic Wolf
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Visual Studio Blog
量子位
The Cloudflare Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
G
Google Developers Blog
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
博客园 - 【当耐特】
C
CERT Recently Published Vulnerability Notes
Scott Helme
Scott Helme
Last Week in AI
Last Week in AI
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
Latest news
Latest news

文章列表 on polarisxu

通过一个例子让你彻底掌握 Go 工作区模式 这道题正确率竟然只有 22% Go 开源项目推荐:一个简单的 Go 练手项目 go test 如何禁用缓存? Go1.18 快讯:constraints 包被移除标准库 跟着 Go 作者学泛型 「2022 版」轻松搞定 Go 开发环境 鹅厂小哥用行动反对加班:程序员加班能被终结吗? Go 为什么选择 Gopher 作为吉祥物? 本地如何配置多个 GitHub/Gitee 账号? 为 Java 开发者准备的 Go 教程 02:Java 有而 Go 无 GitHub 发现了 studygolang 项目依赖的漏洞 Typora 宣布收费后,这款开源 Markdown 编辑器火了 为 Java 开发者准备的 Go 教程 01:漫游了 我的 2021 年终总结 泛型版 singleflight:Go 中如何防止缓存击穿? 2021 年你写了多少代码?这个 Go 工具帮你统计 Go1.18 新特性:TryLock Go 泛型入门教程 程序员瑞士军刀:各种结构的转换工具 终于有 Go 版的 Elasticsearch 了 Go泛型系列:maps 包讲解 Go 1.18 中的 any 是什么? Go1.18 快讯:新的 IP 包 Go编程模式:详解函数式选项模式 Go泛型系列:slices 包讲解 Nginx 竟然也有 playground — Go 语言构建的 Go泛型系列:Go1.18 类型约束那些事 Go1.18 快讯:Module 工作区模式 Go1.18 快讯:新增的 Cut 函数太方便了 Go1.18 快讯:废弃了这个 API Go1.18 快讯:新增字符串 Clone API Go:如何获得项目根目录? Go 如何获取和设置环境变量 重磅变动:更快找到微信公众号里的Go文章 Go 这样设置版本号:我们的项目也可以 假期结束,推荐 2 本 manning 出的 Go 图书 Go Fiber 框架系列教程 04:测试应用 Go Fiber 框架系列教程 03:中间件 Go泛型系列:提前掌握Go泛型的基本使用 推荐一个 Go GUI 实战项目 Go Fiber 框架系列教程 02:详解相关 API 的使用 Go Fiber 框架系列教程 01: 和 Express 对比学习 Go Module:私有不合规库怎么解决引用问题 Go1.17 新特性:go get 变了 Go1.17 新特性:testing 包的相关变化 StackOverflow 上关于 Go select 死锁的问题 新书推荐:用 Gin 框架构建分布式应用 Go 官网要变天。。。 Gin 这是要成为 Go 官方框架? 扬眉吐气:刚刚,Go 已经默认支持泛型了 Go 1.17 新特性:Module 有哪些变化? Go 第三方库推荐:类型转换如此简单 担心密码提交到 GitHub?建议使用这个 Go 开源工具 Go 的时间格式化为什么是 2006-01-02 15:04:05? GitHub 为 Go 社区带来安全支持 Go开源项目推荐:500行代码确认请求时间花在哪 一道关于 len 函数的诡异 Go 面试题解析 厉害了我的 Go:推荐系统都有开源实现 Go1.17 新特性:新版构建约束 这个工具真好:看看你的Go项目依赖有无漏洞 网友很强大,发现了Go并发下载的Bug Rust 劝退系列 09:函数 Go项目实战:一步步构建一个并发文件下载器 这本 Go 新书挺期待的:100 个常见错误 Go 启用新的官方问答社区 这是要干嘛?!微软招 Go 编译器全职开发人员 Go1.17 快报之标准库越来越注重易用性 Go1.17 新特性之切片变数组 回顾 Go 官网的演变史 站长8年前的Go代码竟然进入大厂的项目里了 Go图书翻译:一个好消息,一个坏消息 不怕烂尾!决定组织翻译这本 Go 图书 Rust 劝退系列 08:模式匹配 一本花了2.5年写成的Go免费在线图书 被黑惨了:一句话,说明自己会 Go,咋整? Go1.16 中的新函数 signal.NotifyContext 怎么用? Go 1.17 新特性提前学之测试执行随机化 Rust 劝退系列 07:流程控制 官方的 Go 多版本管理:使用和原理 Rust新书:给你一个劝退的理由 周刊题解:常量表达式这个规则应该了解下 盘点那些使用 Go 语言的国外公司 编写了50万行Go代码是一种什么体验 Rust 劝退系列 06:常量 Uber 使用 Go 的规模这么大?!都自己定制的 Go 编译器了 Go Team Leader — rsc 大神新开源了一个库,增强模板功能 再一次看到了 Go 的节制:int128 类型要不要支持? Rust 劝退系列 05:复合数据类型 「卷」有理论依据:海勒姆定律—Go又是怎么卷的 Go 真的也可以进行 GUI 开发:还有这样的图书呢 Rust 劝退系列 04:基本数据类型 我的 Go 语言书单 Rust 劝退系列 03:变量 我又来推荐免费 Go 新书了:一本用 Go 讲架构的书 注释竟然还有特殊用途?一文解惑 //go:linkname 指令 这个功能,公众号、微信群会不会被玩坏? 一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识 Rust 劝退系列 02:第一个 Rust 程序 Rust 劝退系列 01:打造开发环境
Echo 系列教程——定制篇2:自定义 Validator,进行输入校验
2020-02-28 · via 文章列表 on 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,可以验证任何的多维字段或多层级;
  • 能够深入(多维)了解 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为必填字段

大功告成。

首先,需要定义一个类型,实现 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