Go Web开发之参数校验笔记
# 参数校验
在web开发中,我们经常会对接口的入参
(请求参数)做一些校验,防止用户/前端传入一些不可预期的数据,举个例子,我们有一些添加商品
接口。需要录入商品的信息:
{
"name": "上好佳",
"price": 3.5,
"isbn": "xxxxxx"
}
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
}
...
}
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
定义结构体
以我们上述的代码为例子,我们来改造一下。
type Goods struct {
Name string `json:"name" validate:"required"`
Price float32 `json:"price" validate:"gt=0"`
ISBN string `json:"isbn" validate:"required"`
}
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")
}
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)
}
}
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,最后结果入下图,显示得更友好了。