请求

请求数据概述

请求数据来源分为三类:

  • 路由参数,定义在路由 URI 中的参数,例如 :param*param
  • 查询字符串,URL 中 ? 后的部分,例如 ?id=42
  • 请求主体,主体数据,例如表单数据,AJAX 请求数据,RPC 请求数据等。

其中,请求主体数据通常会有多种数据格式,常见的有:

  • form-data,urlencode 编码的表单数据,例如 key=value&otherKey=>otherValue,通常在浏览器提交表单时生成。请求头为 application/x-www-form-urlencoded
  • JSON,JSON 编码的请求主体,通常的 AJAX 前端会发送该类型数据,同时请求头 Content-Type:Application/json 来表示。最为常见。
  • XML,XML 编码的请求主体,请求头 Content-Type: text/xml
  • YAML,YAML 编码的请求主体,请求头 Content-Type: text/yaml
  • 文件

在处理不同数据来源和不同的数据格式会调用不同的方法,同时支持将请求数据绑定到特定结构类型对象的方法。

路由参数

获取路由参数

定义在 URI 中的参数称为路由参数,请参考 路由->路由参数 章节获取全部内容。注意路由参数非查询字符串,需要使用特定的 c.Param() 函数进行获取:

router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
	})
1
2
3
4
5

模型绑定解析

除了获取特定 key 的参数外,还可以利用 c.ShouldBindUri() 函数直接将路由参数绑定解析到模型上,例如(示例来自 Gin 官网:绑定 Uri):

package main

import "github.com/gin-gonic/gin"
// 定义模型
type Person struct {
    // 字段 类型 `说明 TAG`
    // TAG 中:uri 表示由哪个路由参数绑定,binding 表示绑定说明,例如 required 必须的,uuid UUID 格式。
	ID string `uri:"id" binding:"required,uuid"`
	Name string `uri:"name" binding:"required"`
}

func main() {
	route := gin.Default()
	route.GET("/:name/:id", func(c *gin.Context) {
		var person Person
        // c.ShouldBindUri() 用于绑定,会根据 Person 类型的定义完成
		if err := c.ShouldBindUri(&person); err != nil {
			c.JSON(400, gin.H{"msg": err})
			return
		}
		c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
	})
	route.Run(":8088")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

上面代码中,line 17 会完成绑定到结构体变量 person 中,若绑定成功,person.Name 就是 URI 中 :name 部分,而 person.ID 就是 URI 中 :id 的部分。

除了以上代码中使用的 c.ShouldBindUri() 函数外,还可以使用 c.BindUri() 完成绑定,与 ShouldBindUri 的区别是若发生绑定错误,则会直接终止(Abort)请求处理,响应 400 。

可以在定义 结构体变量时,利用 TAG 对字段进行说明。其中 uri TAG 用来说明绑定来源字段。该操作称之为模型绑定,在 MVC 架构中 person 称为模型,也就是 Model。关于模型绑定和数据验证的内容,参考 模型 章节。

查询字符串

查询字符串 query string ,就是 URL 上问号之后的部分。查询字符串的获取也可以直接获取和绑定获取两种方案。

获取查询字符串

根据查询字符串的格式不同,提供了三(四个)个方法,分别获取标量参数,数组参数和映射参数,还有一个是获取标量参数不得时设置默认值的:

// 获取标量参数,QS 形式为:`key=value`
func (c *Context) Query(key string) string
// 获取数组参数,QS 形式为:`langs=zh-cn&langs=en-us`
// 获取结果为:langs: []string{"zh-cn", "en-us"}
func (c *Context) QueryArray(key string) []string
// 获取映射参数,QS 形式为:`color[menu]=red&color[footer]=gray`
// 获取结果为:color: map[string]string{"menu":"red", "footer":"gray"}
func (c *Context) QueryMap(key string) map[string]string
// 获取标量参数,若 key 不存在,则使用默认值。
func (c *Context) DefaultQuery(key, defaultValue string) string
1
2
3
4
5
6
7
8
9
10

以上四个方法,用于获取 key 对应的查询参数。只是需要使用不同的方法来获取数组和映射表类型的参数。注意一点,获取的参数值都是字符串类型,若需要作为其他类型使用,注意使用类型转换,字符串转换为其他类型,使用 strconv 来实现。

演示代码如下:

//curl http://localhost:8088/queryString?name=hank&langs=zh-cn&langs=en-us&color[menu]=red&color[footer]=gray

func main() {
	route := gin.Default()
	route.GET("/queryString", func(c *gin.Context) {
        c.Query("name") // "hank"
        c.DefaultQuery("age", "42") // "42"
        c.QueryArray("langs") // []string{"zh-cn", "en-us"}
        c.QueryMap("color") // map[string]string{"menu":"red", "footer":"gray"}
	})
	route.Run(":8088")
}
1
2
3
4
5
6
7
8
9
10
11
12

模型绑定解析

同路由参数的解析绑定一致,查询字符串也支持直接将数据绑定到某个特定的结构体类型上。以上示例中,可以定义结构体,完成绑定解析:

//curl http://localhost:8088/queryString?name=hank&langs=zh-cn&langs=en-us&color[menu]=red&color[footer]=gray

type Person struct {
    Name string `form:"name"`
    Age int `form:"age"`
    Langs []string `form:"langs"`
    color map[string]string `form:"color"`
}

func main() {
	route := gin.Default()
	route.GET("/queryString", func(c *gin.Context) {
        var person Person
        if err := c.ShouldBind(&person); err != nil {
            // 绑定错误
            // c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
        }  
        persion.Name // "hank"
        persion.Age // "0"
        person.Langs // []string{"zh-cn", "en-us"}
        persion.Color // map[string]string{"menu":"red", "footer":"gray"}
	})
	route.Run(":8088")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

c.ShouldBind() 函数会完成将请求的查询字符串绑定到 person 上。注意该方法不但可以用在 GET 请求方式使用查询字符串进行绑定,还可以用在 POST、PUT 请求方法中,基于请求主体数据做绑定,是一个使用率非常高的函数。

若要明确指定仅仅从查询字符串的数据进行模型绑定,可以使用函数 c.ShouldBindQuery() 来明确告知。关于模型绑定和数据验证的内容,参考 模型 章节。

主体数据

若需要获取请求主体中的数据,例如在 PUT 和 POST 请求中。请求主体中的数据通常会采用 form-data 和 json 编码,也可能是 xml ,yaml 或 protobuf 等任何类型。获取主体数据通常也是绑定数据到某个结构体对象(称为模型)上。

form-data 数据类型获取

在典型的浏览器提交 post 表单业务逻辑中,此处特指浏览器提交请求而不是通过 ajax 发出请求。请求主体的数据为 form-data 编码,此时我们可以单独的通过 key 或者 绑定到特定结构体上。对于该类型请求,请求头中对类型的描述通常为:Content-Type: application/x-www-form-urlencoded。

若需要通过 key 来获取 form-data 字段,根据字段值的类型不同,使用下面的方法进行获取:

// 获取标量参数,形式为:`key=value`
func (c *Context) PostForm(key string) string
// 获取数组参数,形式为:`langs=zh-cn&langs=en-us`
// 获取结果为:langs: []string{"zh-cn", "en-us"}
func (c *Context) DefaultPostForm(key, defaultValue string) string
// 获取映射参数,形式为:`color[menu]=red&color[footer]=gray`
// 获取结果为:color: map[string]string{"menu":"red", "footer":"gray"}
func (c *Context) PostFormArray(key string) []string
// 获取标量参数,若 key 不存在,则使用默认值。
func (c *Context) PostFormMap(key string) map[string]string
1
2
3
4
5
6
7
8
9
10

大家会发现,与获取查询字符串的方法是一致的,可以获取标量,数组,映射和设置默认值操作。

示例代码:

name := c.PostForm("name")
message := c.DefaultPostForm("message")
1
2

模型绑定解析

请求主体数据可以直接使用 c.ShouldBind() 函数直接绑定解析到模型上。语法与绑定解析查询字符串参数是一致的。示例代码为:

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("/bindBody", func(*gin.Context){
        var person Person
        if err := c.ShouldBind(&person); err != nil {
           // 处理错误
        }
        // person 与请求数据保持对应
	})
	route.Run(":8088")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在使用 c.ShouldBind() 函数时,我们不用考虑数据的格式,是 form-data 还是 JSON 或是其他类型,因为该函数会自动根据请求方法和请求头中 Content-Type 的描述,选择合适的解释器,意味着如下的请求都是可以绑定成功的:

// POST 请求,form-data
GET /testing
Content-Type: applicatioin/x-www-form-urlencoded

name=appleboy&address=xyz&birthday=1992-03-15

// POST 请求,JSON
GET /testing
Content-Type: applicatioin/json

{"name":"appleboy","address":"xyz","birthday":"1992-03-15"}

// 别忘了也包含 GET 请求
GET /testing?name=appleboy&address=xyz&birthday=1992-03-15
1
2
3
4
5
6
7
8
9
10
11
12
13
14

该方法 c.ShouldBind() 也是在获取请求主体数据时,最常用的函数。这就要求我们定义好满足格式的请求数据和结构体类型。

除了这个自动判断主体类型的自动绑定外,我们还可以明确表示由何种数据格式进行解析绑定,使用如下函数:

// 从 JSON 格式绑定
func (c *Context) ShouldBindJSON(obj interface{}) error
// 从 XML 格式绑定
func (c *Context) ShouldBindXML(obj interface{}) error
// 从 YAML 格式绑定
func (c *Context) ShouldBindYAML(obj interface{}) error
// 自定义来源数据格式绑定
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error
// 自定义来源数据格式绑定,同时将 body 设置在 Context 上,可以被重用。
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error)
1
2
3
4
5
6
7
8
9
10

还有,以上全部的 ShouldBind 函数都有与之对应的不以 Should 为前缀的方法,区别在于 Should 前缀方法一旦绑定出错,仅仅会返回错误,而非 Should 前缀方法会在返回错误前,调用 c.Abort() 函数来中止响应处理。支持的函数为:

func (c *Context) BindJSON(obj interface{}) error
func (c *Context) BindQuery(obj interface{}) error
func (c *Context) BindUri(obj interface{}) error
func (c *Context) BindWith(obj interface{}, b binding.Binding) error
func (c *Context) BindXML(obj interface{}) error
func (c *Context) BindYAML(obj interface{}) error
func (c *Context) Bind(obj interface{}) error
func (c *Context) BindHeader(obj interface{}) error
1
2
3
4
5
6
7
8

这些非 Should 前缀函数内部都是调用 MustBindWith() 来实现的。

虽然函数很多,但大多数情况下,我们仅仅需要使用 c.ShouldBind()c.Bind() 即可,一个是返回错误,一个是先 Abort 再返回错误,来进行请求主体数据的绑定。

关于模型绑定和数据验证的内容,参考 模型 章节。

上传文件

若请求主体为 multipart/form-data ,可以通过请求向服务器上传文件,gin 框架可以方便的获取文件。通常区别于每个请求上传的文件数量,调用的方法不同。

单文件

使用 *gin.Context.FormFile() 函数来获取对应的文件信息,代码为:

router.POST("/upload", func(c *gin.Context) {
		// 单文件
		file, _ := c.FormFile("file")
		// 上传文件至指定目录
		c.SaveUploadedFile(file, dst)
	})
1
2
3
4
5
6

其中函数 c.SaveUploadedFile(file, dst) 用于将上传到服务器端的临时文件,保持到特定位置。

c.FormFile("file") 函数返回的是 *multipart.FileHeader 类型,内包含了文件的信息和临时文件位置,结构如下:

type FileHeader struct {
	Filename string // 文件名
	Header   textproto.MIMEHeader // 头信息
	Size     int64 // 大小

	content []byte // 
	tmpfile string // 临时文件位置
}
1
2
3
4
5
6
7
8

我们可以基于该结构进行文件尺寸和类型的校验,通过后,再将该临时文件持久存储。注意,关于文件的真实类型信息,往往不能完全依赖于该数据,因为该数据的内容是来自于浏览器的。我们通常需要基于文件内容做真实的类型校验才可以,

常用方法索引

c.Param()

c.ShouldBindUri()

c.BindUri()

c.Query()

c.DefaultQuery()

c.QueryArray()

c.QueryMap()

c.PostForm()

c.DefaultPostForm()

c.PostFormArray()

c.PostFormMap()

c.ShouldBind()

c.Bind()

c.ShouldBindQuery()

c.BindQuery()