GoWeb开发的gin框架学习 part three

Posted by OuterCyrex on June 3, 2024

三.绑定与验证

一.绑定

绑定参数(通常指的是从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-datax-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())即在getObjElem字段通过名字查询是否存在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验证器来验证用户名是否等于OuterCyrex

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()的每个字符串进行判断,看其是否等于OuterCyrex

以此来达到限制用户名内容的效果。

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()
}