2.5 为接口做参数校验

接下来我们将正式进行编码,在进行对应的业务模块开发时,第一步要考虑到的问题的就是如何进行入参校验,我们需要将整个项目,甚至整个团队的组件给定下来,形成一个通用规范,在今天本章节将核心介绍这一块,并完成标签模块的接口的入参校验。

2.5.1 validator 介绍

在本项目中我们将使用开源项目 go-playground/validator 作为我们的本项目的基础库,它是一个基于标签来对结构体和字段进行值验证的一个验证器。

那么,我们要单独引入这个库吗,其实不然,因为我们使用的 gin 框架,其内部的模型绑定和验证默认使用的是 go-playground/validator 来进行参数绑定和校验,使用起来非常方便。

在项目根目录下执行命令,进行安装:

  1. $ go get -u github.com/go-playground/validator/v10

2.5.2 业务接口校验

接下来我们将正式开始对接口的入参进行校验规则的编写,也就是将校验规则写在对应的结构体的字段标签上,常见的标签含义如下:

标签含义
required必填
gt大于
gte大于等于
lt小于
lte小于等于
min最小值
max最大值
oneof参数集内的其中之一
len长度要求与 len 给定的一致

2.5.2.1 标签接口

我们回到项目的 internal/service 目录下的 tag.go 文件,针对入参校验增加绑定/验证结构体,在路由方法前写入如下代码:

  1. type CountTagRequest struct {
  2. Name string `form:"name" binding:"max=100"`
  3. State uint8 `form:"state,default=1" binding:"oneof=0 1"`
  4. }
  5. type TagListRequest struct {
  6. Name string `form:"name" binding:"max=100"`
  7. State uint8 `form:"state,default=1" binding:"oneof=0 1"`
  8. }
  9. type CreateTagRequest struct {
  10. Name string `form:"name" binding:"required,min=3,max=100"`
  11. CreatedBy string `form:"created_by" binding:"required,min=3,max=100"`
  12. State uint8 `form:"state,default=1" binding:"oneof=0 1"`
  13. }
  14. type UpdateTagRequest struct {
  15. ID uint32 `form:"id" binding:"required,gte=1"`
  16. Name string `form:"name" binding:"min=3,max=100"`
  17. State uint8 `form:"state" binding:"required,oneof=0 1"`
  18. ModifiedBy string `form:"modified_by" binding:"required,min=3,max=100"`
  19. }
  20. type DeleteTagRequest struct {
  21. ID uint32 `form:"id" binding:"required,gte=1"`
  22. }

在上述代码中,我们主要针对业务接口中定义的的增删改查和统计行为进行了 Request 结构体编写,而在结构体中,应用到了两个 tag 标签,分别是 form 和 binding,它们分别代表着表单的映射字段名和入参校验的规则内容,其主要功能是实现参数绑定和参数检验。

2.5.2.2 文章接口

接下来到项目的 internal/service 目录下的 article.go 文件,针对入参校验增加绑定/验证结构体。这块与标签模块的验证规则差不多,主要是必填,长度最小、最大的限制,以及要求参数值必须在某个集合内的其中之一,因此不再赘述。

2.5.3 国际化处理

2.5.3.1 编写中间件

go-playground/validator 默认的错误信息是英文,但我们的错误信息不一定是用的英文,有可能要简体中文,做国际化的又有其它的需求,这可怎么办,在通用需求的情况下,有没有简单又省事的办法解决呢?

如果是简单的国际化需求,我们可以通过中间件配合语言包的方式去实现这个功能,接下来我们在项目的 internal/middleware 目录下新建 translations.go 文件,用于编写针对 validator 的语言包翻译的相关功能,新增如下代码:

  1. import (
  2. "github.com/gin-gonic/gin"
  3. "github.com/gin-gonic/gin/binding"
  4. "github.com/go-playground/locales/en"
  5. "github.com/go-playground/locales/zh"
  6. "github.com/go-playground/locales/zh_Hant_TW"
  7. "github.com/go-playground/universal-translator"
  8. validator "github.com/go-playground/validator/v10"
  9. en_translations "github.com/go-playground/validator/v10/translations/en"
  10. zh_translations "github.com/go-playground/validator/v10/translations/zh"
  11. )
  12. func Translations() gin.HandlerFunc {
  13. return func(c *gin.Context) {
  14. uni := ut.New(en.New(), zh.New(), zh_Hant_TW.New())
  15. locale := c.GetHeader("locale")
  16. trans, _ := uni.GetTranslator(locale)
  17. v, ok := binding.Validator.Engine().(*validator.Validate)
  18. if ok {
  19. switch locale {
  20. case "zh":
  21. _ = zh_translations.RegisterDefaultTranslations(v, trans)
  22. break
  23. case "en":
  24. _ = en_translations.RegisterDefaultTranslations(v, trans)
  25. break
  26. default:
  27. _ = zh_translations.RegisterDefaultTranslations(v, trans)
  28. break
  29. }
  30. c.Set("trans", trans)
  31. }
  32. c.Next()
  33. }
  34. }

在自定义中间件 Translations 中,我们针对 i18n 利用了第三方开源库去实现这块功能,分别如下:

  • go-playground/locales:多语言包,是从 CLDR 项目(Unicode 通用语言环境数据存储库)生成的一组多语言环境,主要在 i18n 软件包中使用,该库是与 universal-translator 配套使用的。
  • go-playground/universal-translator:通用翻译器,是一个使用 CLDR 数据 + 复数规则的 Go 语言 i18n 转换器。
  • go-playground/validator/v10/translations:validator 的翻译器。

而在识别当前请求的语言类别上,我们通过 GetHeader 方法去获取约定的 header 参数 locale,用于判别当前请求的语言类别是 en 又或是 zh,如果有其它语言环境要求,也可以继续引入其它语言类别,因为 go-playground/locales 基本上都支持。

在后续的注册步骤,我们调用 RegisterDefaultTranslations 方法将验证器和对应语言类型的 Translator 注册进来,实现验证器的多语言支持。同时将 Translator 存储到全局上下文中,便于后续翻译时的使用。

2.5.3.2 注册中间件

回到项目的 internal/routers 目录下的 router.go 文件,新增中间件 Translations 的注册,新增代码如下:

  1. func NewRouter() *gin.Engine {
  2. r := gin.New()
  3. r.Use(gin.Logger())
  4. r.Use(gin.Recovery())
  5. r.Use(middleware.Translations())
  6. ...
  7. }

至此,我们就完成了在项目中的自定义验证器注册、验证器初始化、错误提示多语言的功能支持了。

2.5.4 接口校验

我们在项目下的 pkg/app 目录新建 form.go 文件,写入如下代码:

  1. import (
  2. ...
  3. ut "github.com/go-playground/universal-translator"
  4. val "github.com/go-playground/validator/v10"
  5. )
  6. type ValidError struct {
  7. Key string
  8. Message string
  9. }
  10. type ValidErrors []*ValidError
  11. func (v *ValidError) Error() string {
  12. return v.Message
  13. }
  14. func (v ValidErrors) Error() string {
  15. return strings.Join(v.Errors(), ",")
  16. }
  17. func (v ValidErrors) Errors() []string {
  18. var errs []string
  19. for _, err := range v {
  20. errs = append(errs, err.Error())
  21. }
  22. return errs
  23. }
  24. func BindAndValid(c *gin.Context, v interface{}) (bool, ValidErrors) {
  25. var errs ValidErrors
  26. err := c.ShouldBind(v)
  27. if err != nil {
  28. v := c.Value("trans")
  29. trans, _ := v.(ut.Translator)
  30. verrs, ok := err.(val.ValidationErrors)
  31. if !ok {
  32. return false, errs
  33. }
  34. for key, value := range verrs.Translate(trans) {
  35. errs = append(errs, &ValidError{
  36. Key: key,
  37. Message: value,
  38. })
  39. }
  40. return false, errs
  41. }
  42. return true, nil
  43. }

在上述代码中,我们主要是针对入参校验的方法进行了二次封装,在 BindAndValid 方法中,通过 ShouldBind 进行参数绑定和入参校验,当发生错误后,再通过上一步在中间件 Translations 设置的 Translator 来对错误消息体进行具体的翻译行为。

另外我们声明了 ValidError 相关的结构体和类型,对这块不熟悉的读者可能会疑惑为什么要实现其对应的 Error 方法呢,我们简单来看看标准库中 errors 的相关代码,如下:

  1. func New(text string) error {
  2. return &errorString{text}
  3. }
  4. type errorString struct {
  5. s string
  6. }
  7. func (e *errorString) Error() string {
  8. return e.s
  9. }

标准库 errors 的 New 方法实现非常简单,errorString 是一个结构体,内含一个 s 字符串,也只有一个 Error 方法,就可以认定为 error 类型,这是为什么呢?这一切的关键都在于 error 接口的定义,如下:

  1. type error interface {
  2. Error() string
  3. }

在 Go 语言中,如果一个类型实现了某个 interface 中的所有方法,那么编译器就会认为该类型实现了此 interface,它们是”一样“的。

2.5.5 验证

我们回到项目的 internal/routers/api/v1 下的 tag.go 文件,修改获取多个标签的 List 接口,用于验证 validator 是否正常,修改代码如下:

  1. func (t Tag) List(c *gin.Context) {
  2. param := struct {
  3. Name string `form:"name" binding:"max=100"`
  4. State uint8 `form:"state,default=1" binding:"oneof=0 1"`
  5. }{}
  6. response := app.NewResponse(c)
  7. valid, errs := app.BindAndValid(c, &param)
  8. if !valid {
  9. global.Logger.Errorf("app.BindAndValid errs: %v", errs)
  10. response.ToErrorResponse(errcode.InvalidParams.WithDetails(errs.Errors()...))
  11. return
  12. }
  13. response.ToResponse(gin.H{})
  14. return
  15. }

在命令行中利用 CURL 请求该接口,查看验证结果,如下:

  1. $ curl -X GET http://127.0.0.1:8000/api/v1/tags\?state\=6
  2. {"code":10000001,"details":["State 必须是[0 1]中的一个"],"msg":"入参错误"}

另外你还需要注意到 TagListRequest 的校验规则里其实并没有 required,因此它的校验规则应该是有才校验,没有该入参的话,是默认无校验的,也就是没有 state 参数,也应该可以正常请求,如下:

  1. $ curl -X GET http://127.0.0.1:8000/api/v1/tags
  2. {}

在 Response 中我们调用的是 gin.H 作为返回结果集,因此该输出结果正确。

2.5.6 小结

在本章节中,我们介绍了在 gin 框架中如何通过 validator 来进行参数校验,而在一些定制化场景中,我们常常需要自定义验证器,这个时候我们可以通过实现 binding.Validator 接口的方式,来替换其自身的 validator::

  1. // binding/binding.go
  2. type StructValidator interface {
  3. ValidateStruct(interface{}) error
  4. Engine() interface{}
  5. }
  6. func setupValidator() error {
  7. // 将你所自定义的 validator 写入
  8. binding.Validator = global.Validator
  9. return nil
  10. }

也就是说如果你有定制化需求,也完全可以自己实现一个验证器,效仿我们前面的模式,就可以完全替代 gin 框架原本的 validator 使用了。

而在章节的后半段,我们对业务接口进行了入参校验规则的编写,并且针对错误提示的多语言化问题(也可以理解为一个简单的国际化需求),通过中间件和多语言包的方式进行了实现,在未来如果你有更细致的国际化需求,也可以进一步的拓展。

本图书由 煎鱼©2020 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。

2.5 为接口做参数校验 - 图1