Go(Gin框架):03—Gin数据解析和绑定(Bind与ShouldBind系列函数)

一、介绍

  • gin框架提供了相关接口,可以用来解析HTTP请求中的各种数据,然后将解析出来的数据绑定到Go语言中的结构体上
  • 目前支持JSON、XML、YAML和标准表单值(foo=bar&boo=baz)的绑定
  • gin使用https://github.com/go-playground/validator验证参数,查看完整文档https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags

结构体成员标签

  • Go语言中的结构体有一个"成员标签(field tag)"语法
  • Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来
  • 例如,下面的结构体Name成员都一系列标签,其中:
    • 绑定到JSON中时,对应JSON中的name字段
    • 绑定到DB中时,对应DB中的db_name字段
    • 绑定到INI中时,对应INI中的ini_name字段
1
2
3
type person struct {
    Name string 'json: "name" db: "db_name" ini: "ini_name"'
}

  • 本文不是着重讲解该语法的,可以参阅本文在另一篇文章中对结构体标签的讲解https://dongshao.blog.csdn.net/article/details/110677325
  • 备注:如果一个字段用binding:"required"修饰,并且在绑定时该字段的值为空,那么将返回一个错误
1
2
3
type Login struct {
    Name string `form: "user_name" xml: "name" binding: "required"`
}

两套绑定方法

  • 第一类是以"ShouldBind"系列开头的方法:这些方法底层使用ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误

  • 第二类是以"Bind"系列开头的方法:
    • 这些方法底层使用MustBindWith
    • 如果存在绑定错误,请求将被以下指令中止 c.AbortWithError(400, err).SetType(ErrorTypeBind),响应状态代码会被设置为400,请求头Content-Type被设置为text/plain; charset=utf-8
    • 注意,如果你试图在此之后设置响应代码,将会发出一个警告 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422,如果你希望更好地控制行为,请使用ShouldBind相关的方法

  • 当我们使用绑定方法时,Gin会根据Content-Type推断出使用哪种绑定器。如果你确定你绑定的是什么,你可以使用MustBindWith或者BindingWith

二、演示:JSON数据解析和绑定

ShouldBindJSON

  • 该方法绑定解析JSON数据
1
2
3
func (c *Context) ShouldBindJSON(obj interface{}) error {
    return c.ShouldBindWith(obj, binding.JSON)
}

演示案例

  • 代码如下:
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
package main

import "C"
import (
    "github.com/gin-gonic/gin"
    "net/http"
)

// 绑定数据用的结构体
type Login struct {
    // 每个成员后面都有标签, 表示在解析对应数据格式时绑定到对方的字段名
    // 注意, 这里使用到了binding标签
    Name string `form: "user_name" json: "name" uri: "name" xml: "name" binding: "required"`
    Password string `form: "user_password" json: "password" uri: "password" xml: "password" binding: "required"`
}

func main() {
    r := gin.Default()

    // 处理POST请求
    r.POST("/loginJson", func(c *gin.Context) {
        // 创建一个结构体, 用来保存数据
        var loginResult Login

        // 对端发来的数据是JSON格式的, 那么调用这个函数将对端的JSON数据绑定到结构体上
        if err := c.ShouldBindJSON(&loginResult); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
        }

        // 判断账号密码是否正确, 然后给对端返回结果
        if loginResult.Name != "dongshao" && loginResult.Password != "123456" {
            c.JSON(http.StatusBadRequest, gin.H{ "status": "304"})
        } else {
            c.JSON(http.StatusOK, gin.H{ "status": "200"})
        }

    })

    r.Run(":8000")
}
  • 运行程序:

  • 构造HTTP请求,数据格式为"application/json",然后在实体部分发送row类型JSON消息

  • 发送一个POST请求,使用正确的账号和密码

  • 发送一个POST请求,使用错误的账号和密码

三、演示:XML数据解析和绑定

演示案例

  • 代码如下:
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
package main

import "C"
import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type Login struct {
    Name string `form:"username" json:"name" uri:"name" xml:"name" binding:"required"`
    Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}

func main() {
    r := gin.Default()

    // 处理POST请求
    r.POST("/loginXML", func(c *gin.Context) {
        var loginResult Login

        // 调用Bind()函数, 该函数默认绑定解析Form表单数据
        if err := c.ShouldBindXML(&loginResult); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
            return
        }

        // 判断账号密码是否正确, 然后给对端返回结果
        if loginResult.Name != "dongshao" || loginResult.Password != "123456" {
            c.JSON(http.StatusBadRequest, gin.H{ "status": "304"})
        } else {
            c.JSON(http.StatusOK, gin.H{ "status": "200"})
        }
    })

    r.Run(":8000")
}

  • 运行程序:

  • 构造HTTP请求:请求头的Context-Type字段的值设置为"application/xml",实体部分输入下面的XML文件内容,响应成功

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <name>dongshao</name>
    <password>123456</password>
</root>

四、演示:只绑定GET参数

ShouldBindQuery

  • 该方法只能用来绑定GET数据,不能绑定其他类型请求的数据
1
func (c *Context) ShouldBindQuery(obj interface{}) error

演示案例

  • 代码如下:
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
package main

import "C"
import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type Person struct {
    Name    string `form:"name"`
    Address string `form:"address"`
}

func main() {
    r := gin.Default()

    // 处理GET请求
    r.GET("/testing", func(c *gin.Context) {
        var person Person

        // 绑定GET
        if err := c.ShouldBindQuery(&person); err != nil {
            c.String(http.StatusBadRequest, err.Error())
            return
        }
        message := "name: " + person.Name + "address: " + person.Address
        c.String(http.StatusOK, message)
    })

    r.Run(":8000")
}
  • 运行程序:

  • 构造GET请求,效果如下:

五、Bind/ShouldBind:绑定GET或者POST、绑定POST、绑定HTML

  • GET或POST参数,HTML表单等参数可以使用这两个方法进行绑定

绑定GET参数

  • 在GET()请求中调用该方法,其即可以解析URL中的GET参数,也可以解析实体部分的表单数据;如果GET参数和实体部分的表单数据参数重复,那么优先处理URL中的GET参数(见下面演示)
  • 代码如下:
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
package main

import (
    "fmt"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

type Person struct {
    Name     string    `form:"name"`
    Address  string    `form:"address"`
    Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func main() {
    route := gin.Default()
    route.GET("/testing", func(c *gin.Context) {
        var person Person

        if err := c.ShouldBind(&person); err != nil {
            c.String(http.StatusBadRequest, err.Error())
            return
        }
        message := "name: " + person.Name + ", address: " + person.Address + ", birthday: " + fmt.Sprint(person.Birthday)
        c.String(http.StatusOK, message)
    })
    route.Run(":8000")
}
  • 构造GET请求,只有URL中有参数

  • 构造GET请求:只有实体部分有参数

  • 构造GET请求:URL中和实体部分都有参数,那么优先处理URL中的GET参数

绑定POST参数

  • 在POST()请求中调用该方法,其即可以解析URL中的参数,也可以解析实体部分的表单数据;如果URL中的参数和实体部分的表单数据参数重复,那么优先处理实体部分的表单数据(见下面演示)
  • 代码如下:
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
package main

import (
    "github.com/gin-gonic/gin"
)

type LoginForm struct {
    User     string `form:"user" binding:"required"`
    Password string `form:"password" binding:"required"`
}

func main() {
    router := gin.Default()
    router.POST("/login", func(c *gin.Context) {
        var form LoginForm
        if c.ShouldBind(&form) == nil {
            if form.User == "dongshao" && form.Password == "password" {
                c.JSON(200, gin.H{"status": "you are logged in"})
            } else {
                c.JSON(401, gin.H{"status": "unauthorized"})
            }
        }
    })
    router.Run(":8000")
}
  • 运行程序,然后构造POST请求,本次只有URL中有参数,响应成功

  • 构造POST请求,本次只有实体部分有参数,响应成功

  • 构造POST请求,本次URL和实体部分都有数据,优先响应实体部分的

表单数据处理演示1:绑定HTML复选框

  • 编写下面的HTML文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>复选框</title>
</head>

<body>

<form action="http://127.0.0.1:8000/test" method="POST">
    <p>Check some colors</p>
    <label for="red">Red</label>
    <input type="checkbox" name="colors[]" value="red" id="red">
    <label for="green">Green</label>
    <input type="checkbox" name="colors[]" value="green" id="green">
    <label for="blue">Blue</label>
    <input type="checkbox" name="colors[]" value="blue" id="blue">
    <input type="submit">
</form>

</body>
</html>
  • Go代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "github.com/gin-gonic/gin"
)

type myForm struct {
    Colors []string `form:"colors[]"`
}

func main() {
    route := gin.Default()

    route.POST("/test", func(c *gin.Context) {
        var fakeForm myForm
        c.ShouldBind(&fakeForm)
        c.JSON(200, gin.H{"color": fakeForm.Colors})
    })

    route.Run(":8000")
}
  • 运行程序,然后使用网页进行访问,结果如下

表单数据处理演示2

  • 编写下面的HTML文件,内容如下,表单的enctype为multipart/form-data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>

<body>

<form action="http://127.0.0.1:8000/loginForm" method="post" enctype="multipart/form-data">
    用户名:<input type="text" name="username">
    <br>
    密&nbsp&nbsp码<input type="password" name="password">
    <input type="submit" value="登录">
    </from>
</body>
</html>
  • Go代码如下
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
package main

import "C"
import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type Login struct {
    Name string `form:"username" json:"name" uri:"name" xml:"name" binding:"required"`
    Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}

func main() {
    r := gin.Default()

    // 处理POST请求
    r.POST("/loginForm", func(c *gin.Context) {
        var loginResult Login

        // 调用Bind()函数, 该函数默认绑定解析Form表单数据
        if err := c.Bind(&loginResult); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
            return
        }

        // 判断账号密码是否正确, 然后给对端返回结果
        if loginResult.Name != "dongshao" || loginResult.Password != "123456" {
            c.JSON(http.StatusBadRequest, gin.H{ "status": "304"})
        } else {
            c.JSON(http.StatusOK, gin.H{ "status": "200"})
        }
    })

    r.Run(":8000")
}
  • 运行程序

  • 打开网页访问,成功

六、演示:URI数据解析和绑定

ShouldBindJSON

  • 该方法绑定解析Uri数据
1
func (c *Context) ShouldBindUri(obj interface{}) error

演示案例

  • Go代码如下:
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 "C"
import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type Login struct {
    Name string `form:"username" json:"name" uri:"name" xml:"name" binding:"required"`
    Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}

func main() {
    r := gin.Default()

    // 此处为GET请求
    // 设置URI规则
    r.GET("/:name/:password", func(c *gin.Context) {
        var loginResult Login

        // 绑定解析Uri数据
        if err := c.ShouldBindUri(&loginResult); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error() })
            return
        }

        // 判断账号密码是否正确, 然后给对端返回结果
        if loginResult.Name != "dongshao" || loginResult.Password != "admin" {
            c.JSON(http.StatusBadRequest, gin.H{ "status": "304"})
        } else {
            c.JSON(http.StatusOK, gin.H{ "status": "200"})
        }
    })

    r.Run(":8000")
}
  • 运行程序,使用浏览器访问,输入指定的URI,运行成功