Go参数校验.md

2022/6/13 Go

Go Web开发之参数校验笔记

# 参数校验

在web开发中,我们经常会对接口的入参(请求参数)做一些校验,防止用户/前端传入一些不可预期的数据,举个例子,我们有一些添加商品接口。需要录入商品的信息:

{
  "name": "上好佳",
  "price": 3.5,
  "isbn": "xxxxxx"
}
1
2
3
4
5

这里我们需要前端传入商品的名称价格isbn信息,假设我们要实现这样一个api,如果手动编写参数校验代码的话,我们以gin框架为例子,可能会是这样:

type Goods struct {
    Name string `json:"name"`
    Price float32 `json:"price"`
    ISBN string `json:"isbn"`
}

func AddGoods(ctx *gin.Context) {
    var good Goods
    if err := ctx.ShouldBindJSON(&good); err != nil {
        ctx.JSON(200, gin.H{"code": "110", "msg": "参数传入有误"})
        return
    }
    if good.name == "" {
        ctx.JSON(200, gin.H{"code": "110", "msg": "商品名称不能为空"})
        return
    }
    if good.price < 0 {
        ctx.JSON(200, gin.H{"code": "110", "msg": "商品价格不能小于0"})
        return
    }
    if good.isbn == "" {
        ctx.JSON(200, gin.H{"code": "110", "msg": "商品isbn不能为空"})
        return
    }
    
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

可以看到,这还只是字段比较少的情况,如果字段多,并且想对每个字段都进行校验,那我们得写多少个if/else,这个还是不敢想象。

我们借用网上一张图来看看参数校验的地狱:

# gin自带的参数校验

其实gin本身是自带了参数校验功能的,不过我个人感觉不是很明了。我们来看看demo (opens new window)

它的方法是利用binding标签,给字段绑定需要校验的规则,这个和我们接下来要说到的validator很相似。由于我没有实际用过gin自带的校验,所以这个就有待大家自行探索了。

# Validator

validator是一款基于标签+反射,实现对字段校验的库,他们自己做过一些性能测试,虽然利用了反射,但是相对来说损耗也没有想象的大,对我们平时web开发来说也是没有多少影响的。

接着我们根据官网的文档 (opens new window)来一探究竟吧~

  • 安装validator
go get github.com/go-playground/validator/v10
1
  • 定义结构体

    以我们上述的代码为例子,我们来改造一下。

type Goods struct {
	Name  string  `json:"name" validate:"required"`
	Price float32 `json:"price" validate:"gt=0"`
	ISBN  string  `json:"isbn" validate:"required"`
}
1
2
3
4
5

这里的required代表必填字段,gt代表大于,我们写个简单的接口:

func main() {
    app := gin.Default()
    app.POST("/v", func(ctx *gin.Context) {
            var goods Goods
            if err := ctx.ShouldBindJSON(&goods); err != nil {
                    ctx.JSON(200, gin.H{"code": 110, "msg": "参数解析失败"})
                    return
            }
            validate := validator.New()
            if err := validate.Struct(&goods); err != nil {
                    ctx.JSON(200, gin.H{"code": 110, "msg": err.Error()})
                    return
            }
            ctx.JSON(200, "success")

    })

    app.Run("0.0.0.0:8080")
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可以看到,我通过定义validate标签,以及使用validate.Struct方法,完成了对结构体字段的校验,我们来测试一下:

在只传入isbn的情况下,validator给了提示,提示name和price不满足要求,我们加入name字段试试:

可以看到,目前只有price不满足要求了,我们试一下传入price=-1.0, 可以看到依然是这个报错,等我们把price变成大于0的数据应该就好了:

果然如此。。。

但是其实报错提示对我们来说不是很友好,可以说是特别不可读了!其实官网也提供了国际化的部分,我们可以参考下面的例子 (opens new window)

如果不嫌弃,也可以看看我的demo:

package validate

import (
	"errors"
	"github.com/go-playground/locales/zh"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	translations "github.com/go-playground/validator/v10/translations/zh"
	"log"
	"reflect"
	"strings"
)

var (
	check *validator.Validate
	trans ut.Translator
)

func CheckParams(data interface{}) error {
	if err := check.Struct(data); err != nil {
		errs := err.(validator.ValidationErrors)
		translate := errs.Translate(trans)
		for _, v := range translate {
			return errors.New(v)
		}
	}
	return nil
}

func init() {
	cn := zh.New()
	uni := ut.New(cn, cn)

	// this is usually know or extracted from http 'Accept-Language' header
	// also see uni.FindTranslator(...)
	trans, _ = uni.GetTranslator("zh")
	check = validator.New()
	check.RegisterTagNameFunc(func(fld reflect.StructField) string {
		name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]

		if name == "-" {
			return ""
		}

		return name
	})

	err := translations.RegisterDefaultTranslations(check, trans)
	if err != nil {
		log.Fatal("validate translate error: ", err)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

上面的代码(新建了一个validate.go文件)参考官网的国际化做了一些初始化操作,遇到一个报错就直接return,最后结果入下图,显示得更友好了。