三.绑定与验证
一.绑定
绑定参数(通常指的是从HTTP请求中提取数据并映射到应用程序的某个数据结构,如结构体、类等)允许开发人员轻松地从客户端请求中提取必要的信息,以便在服务器端进行处理。
1.ShouldBindJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "github.com/gin-gonic/gin"
type UserInfo struct {
Name string `json:"name"`
Age int `json:"age"`
Sex string `json:"sex"`
}
func main(){
router := gin.Default()
router.POST("/", func(c *gin.Context) {
var userInfo UserInfo
err := c.ShouldBindJSON(&userInfo)
if err != nil {
c.JSON(200, gin.H{"data": "err"})
return
}
c.JSON(200, gin.H{"data":"OK"})
})
_ = router.Run(":8080")
}
通过ShouldBindJSON
函数,将客户端传入的JSON
数据以UserInfo
的形式提取信息
1
2
3
4
5
6
7
8
9
10
//输入json形式的原始数据
{
"name":"Outer",
"age":18,
"sex":"male"
}
//返回值
{
"data":"OK"
}
2.ShouldBindQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserInfo struct {
Name string `form:"name"`
Age int `form:"age"`
Sex string `form:"sex"`
}//需要在定义结构体时加入form的tag
router.POST("/query", func(c *gin.Context) {
var userinfo UserInfo
err := c.ShouldBindQuery(&userinfo)
if err != nil {
c.JSON(200, gin.H{"data": "err"})
return
}
c.JSON(200, userinfo)
})
通过ShouldBindQuery
函数,将客户端传入的动态参数信息以UserInfo
的形式提取信息
1
2
3
4
5
6
7
//输入localhost:8080/query?name=Outer&age=18&sex=male
//返回值为:
{
"name": "Outer",
"age": 18,
"sex": "male"
}
3.ShouldBindUri
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserInfo struct {
Name string `uri:"name"`
Age int `uri:"age"`
Sex string `uri:"sex"`
}//需要在定义结构体时加入uri的tag
router.POST("/uri/:name/:age/:sex", func(c *gin.Context) {
var userinfo UserInfo
err := c.ShouldBindUri(&userinfo)
if err != nil {
c.JSON(200, gin.H{"data": "err"})
return
}
c.JSON(200, userinfo)
})
通过ShouldBindUri
函数,将客户端传入的Uri
信息以UserInfo
的形式提取信息
1
2
3
4
5
6
7
//输入localhost:8080/uri/Outer/18/male
//返回值为:
{
"name": "Outer",
"age": 18,
"sex": "male"
}
4.ShouldBind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserInfo struct {
Name string `form:"name"`
Age int `form:"age"`
Sex string `form:"sex"`
}//需要在定义结构体时加入form的tag
router.POST("/form", func(c *gin.Context) {
var userinfo UserInfo
err := c.ShouldBind(&userinfo)
if err != nil {
c.JSON(200, gin.H{"data": "err"})
return
}
c.JSON(200, userinfo)
})
可以通过ShouldBind
方法实现对form-data
和x-www-form-urlencoded
的绑定操作
此处需注意ShouldBind
方法的源码:
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
//ShouldBind将传入的数据进行类型的判断
func (c *Context) ShouldBind(obj any) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
//binding.Default的内容:
func Default(method, contentType string) Binding {
if method == http.MethodGet {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML, MIMEYAML2:
return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}
//以form类型作为Default的返回值
二.验证器
1.验证器指令
下述内容为常见的一些验证器指令:
a.针对字符串
- requird :必填字段,不可为空 如
binding:"required"
- min:字符串最小长度 如:
binding:"min=5"
- max:字符串最大长度 如:
binding:"max=10"
- len:字符串长度 如:
binding:"len=6"
- eqfield:等于其他字段的值 如:
Password string binding:"eqfield=ConfirmPassword"
- nefield:不等于其他字段 如:
Name string binding:"nefield=Outer"
- -:忽略字段 如:
binding:"-"
b.针对数字
- eq:等于 如:
binding:"eq=3"
- ne:不等于 如:
binding:"ne=13"
- gt:大于 如:
binding:"gt=13"
- gte:大于等于 如:
binding:"gte=10"
- lt:小于 如:
binding:"lt=13"
- lte:小于等于 如:
binding:"lte=13"
c.字符串内容
- oneof:字段必须要属于
oneof
语句后的情况 如:binding:"oneof=man woman"
- excludes:字符串必须包含语句后边的内容 如:
binding:"excludes=Outer"
- startswith:字符串必须以语句后边的内容开头 如:
binding:"startswith=O"
- endswith:字符串必须以语句后边的内容结尾 如:
binding:"endswith=x"
- dive:对数组内的每一个进行检测 如:
binding:"dive,startswith=O"
- ip:检测字段是否为一段ip地址 如:
binding:"ip"
- ipv4:检测字段是否为一段ipv4地址 如:
binding:"ipv4"
- ipv6:检测字段是否为一段ipv6地址 如:
binding:"ipv6"
- url:检测字段是否为一段url路径 如:
binding:"url"
- uri:检测字段是否为一段uri路径 如:
binding:"uri"
- date:检测字段是否为语句后边对应的时间格式 如:
binding:"date=2006-01-02 15:04:05"
如要创建一个注册信息
需要用户名、密码、重复密码字段。且要求三个字段均不为空,密码需包含至少6个字符,至多18个字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "github.com/gin-gonic/gin"
type User struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6,max=18"`
RePassword string `json:"rePassword" binding:"eqfield=Password"`
}
func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
var user User
err := c.ShouldBindJSON(&user)
if err != nil {
c.JSON(200, gin.H{"code": 400, "message": err.Error()})
return
}
c.JSON(200, gin.H{"code": 200, "data": user})
})
_ = router.Run(":8080")
}
2.自定义错误信息
在实际Web开发中,需要将后端的错误信息返回前端,此时需要能让用户能清晰看出错误原因,因此需要能自定义错误信息。
在这里将上方的代码进行一定的修改:
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
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"reflect"
)
type User struct {
Username string `json:"username" binding:"required" msg:"用户名必填"`
Password string `json:"password" binding:"required,gte=6,lte=18" msg:"密码不合要求"`
RePassword string `json:"rePassword" binding:"eqfield=Password" msg:"重复密码与原密码不同"`
}
func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
var user User
err := c.ShouldBindJSON(&user)
if err != nil {
getObj := reflect.TypeOf(&user)
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
fmt.Println(msg)
}
}
}
c.JSON(200, gin.H{"code": 400, "message": err.Error()})
return
}
c.JSON(200, gin.H{"code": 200, "data": user})
})
_ = router.Run(":8080")
}
此时的返回值:
1
2
3
4
5
6
7
8
9
10
//POST一个json的原始数据
{
"username":"",
"password":"123",
"rePassword":"123456"
}
//返回值:
用户名必填
密码不合要求
重复密码与原密码不同
通过对这个过程进行封装,便于对多次验证器进行错误反馈。
1
2
3
4
5
6
7
8
9
10
11
12
func GetValidator(err error, obj any) string {
getObj := reflect.TypeOf(obj) //传入值应为&struct,即取结构体的地址
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
return msg
}
}
}
return err.Error()
}
对上述的封装内容进行分析:
1
getObj := reflect.TypeOf(obj)
通过go自带的reflect
包来获取传入的结构体指针的类型信息,包括字段和Tag
信息
1
if errs, ok := err.(validator.ValidationErrors); ok
类型断言可以判断传入的err
值是否为需要处理的validator.ValidationErrors
(验证错误),如果断言成功,则err
将被将被赋值为该validator.ValidationErrors
类型的值,且ok
将被赋值为true
。
由于err
信息可能不止一个,则errs
可能为一个切片类型,元素为validator.ValidationErrors
类型的值。此时需要遍历errs
来获得具体的错误信息。
1
2
3
4
5
6
for _, e := range errs {
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
return msg
}
}
getObj.Elem().FieldByName(e.Field())
即在getObj
的Elem
字段通过名字查询是否存在e.Field()
字段,若存在则返回其值以及一个bool
类型。
通过f.Tag.Get()
方法获得对应tag
的信息。
注:
reflect.TypeOf
的返回值为一个Type
类型,内容为:
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
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Name() string
PkgPath() string
Size() uintptr
String() string
Kind() Kind
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
Bits() int
ChanDir() ChanDir
IsVariadic() bool
Elem() Type
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
In(i int) Type
Key() Type
Len() int
NumField() int
NumIn() int
NumOut() int
Out(i int) Type
common() *abi.Type
uncommon() *uncommonType
}
3.自定义验证器
由于gin
框架内置的验证器数量有限,我们可以通过自定义验证器来满足实际开发中的需求
下边将自定义一个signVaild
验证器来验证用户名是否等于Outer
或Cyrex
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
53
54
55
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"reflect"
)
type User struct {
Username string `json:"username" binding:"required,sign" msg:"用户名必填或不符合要求"`
Password string `json:"password" binding:"required,gte=6,lte=18" msg:"密码不合要求"`
RePassword string `json:"rePassword" binding:"eqfield=Password" msg:"重复密码与原密码不同"`
}
func GetValidator(err error, obj any) string {
getObj := reflect.TypeOf(obj)
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
return msg
}
}
}
return err.Error()
}
func signValid(fl validator.FieldLevel) bool {
nameList := []string{"Outer", "Cyrex"}
for _, name := range nameList {
if fl.Field().String() == name {
return false
}
}
return true
}
func main() {
router := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("sign", signValid)
}
router.POST("/login", func(c *gin.Context) {
var user User
err := c.ShouldBindJSON(&user)
if err != nil {
msg := GetValidator(err, &user)
c.JSON(200, gin.H{"err": msg})
}
c.JSON(200, gin.H{"code": 200, "data": user})
})
_ = router.Run(":8080")
}
遍历内容也可以写作:
1
2
3
4
5
6
for _, nameStr := range nameList {
name, _ := fl.Field().Interface().(string)
if name == nameStr {
return false
}
}
下面我们对上述代码进行注释和源码分析:
1
2
3
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("sign", signValid)
}
此处使用了类型断言,断言binding.Validator.Engine()
的类型为*validator.Validate
。这样的话就可以得到一个指向验证器的指针,便于创建自定义验证器。
1
2
3
4
5
6
7
8
//RegisterValidation方法的源码
func (v *Validate) RegisterValidation(tag string, fn Func, callValidationEvenIfNull ...bool) error {
return v.RegisterValidationCtx(tag, wrapFunc(fn), callValidationEvenIfNull...)
}
//一般而言,RegiseterValidation需要一个tag字符串来代表新验证器,还需要一段函数,这个函数必须是
type Func func(fl FieldLevel) bool
//需要输入一个FieldLevel类型,并会返回一个布尔值
// FieldLevel包含验证字段所需的所有信息
之后我们需要定义这个验证器函数
1
2
3
4
5
6
7
8
9
func signValid(fl validator.FieldLevel) bool {
nameList := []string{"Outer", "Cyrex"}
for _, name := range nameList {
if fl.Field().String() == name {
return false
}
}
return true
}
这个过程中,我们对fl.Field()
的每个字符串进行判断,看其是否等于Outer
或Cyrex
以此来达到限制用户名内容的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//FieldLevel这一类型的定义
type FieldLevel interface {
Top() reflect.Value
Parent() reflect.Value
Field() reflect.Value
FieldName() string
StructFieldName() string
Param() string
GetTag() string
ExtractType(field reflect.Value) (value reflect.Value, kind reflect.Kind, nullable bool)
GetStructFieldOK() (reflect.Value, reflect.Kind, bool)
GetStructFieldOKAdvanced(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool)
GetStructFieldOK2() (reflect.Value, reflect.Kind, bool, bool)
GetStructFieldOKAdvanced2(val reflect.Value, namespace string) (reflect.Value, reflect.Kind, bool, bool)
}
也可以通过类型断言来限定interface{}
的类型,效果等同于fl.Field.String()
1
2
3
4
5
6
for _, nameStr := range nameList {
name, _ := fl.Field().Interface().(string)
if name == nameStr {
return false
}
}
注:reflect.Value
包含多种方法,可以返回对应类型的值。如:
1
2
3
4
5
6
7
func (v Value) String() string {
// stringNonString is split out to keep String inlineable for string kinds.
if v.kind() == String {
return *(*string)(v.ptr)
}
return v.stringNonString()
}