Gin 框架学习笔记(02)— 参数自动绑定到结构体
JSON
、XML
、YAML
和标准表单 form
数据 foo=bar&boo=baz
等。换句话说,只要定义好结构体,就可以将请求中包含的数据自动接收过来,这是 Gin
框架非常神奇的功能。
在定义绑定对应的结构体时,需要给结构体字段设置绑定类型的标签,比如绑定 JSON
数据时,设置字段标签为 json:"fieldname"
。使用绑定可以更快捷地把数据传递给程序中的数据对象。
使用 Gin
框架中系列绑定有关方法时,Gin
会根据请求头中 Content-Type
推断如何绑定,也就是自动绑定。但如果明确绑定的类型,开发人员也可以使用 MustBindWith()
方法或 BindJSON()
等方法而不用自动推断。可以指定结构体某字段是必需的,字段需要设置标签 binding:"required"
,但如果绑定时是空值,Gin
会报错。
在 Gin
框架的 binding
包中,定义了 Content-Type
请求头信息的多种 MIME
类型,以便在自动绑定时进行类型判别进而采用对应的处理方法:
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
)
在所有绑定的方法中,首先 c.Bind()
方法会根据 Content-Type
推断得到一个 bindding
实例对象。因为它会调用函数 func Default(method, contentType string) Binding
,这个函数根据 HTTP
请求的方法和 Content-Type
来实例化具体的 bindding
对象。一共可以实例化为下面几种类型:
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
在 binding
包也就是 binding
目录中,可以看到每种实例结构都单独在一个文件定义了系列处理方法。 c.Bind()
方法得到 binding
实例对象后,会调用 c.MustBindWith(obj, b)
方法, b
为实例化的某类 binding
对象,而像 c.BindJSON()
方法由于知道实例化对象是 JSON
,所以也调用 c.MustBindWith(obj, b)
,这里的 b
是 jsonBinding{}
对象。其他像 XML
等的处理过程类似。
而 c.MustBindWith()
方法会统一调用 c.ShouldBindWith()
方法,在 c.ShouldBindWith()
方法中会调用具体实例的处理方法: b.Bind(c.Request, obj)
,这个 b.Bind()
方法很关键,每种 binding
实例对象都有实现这个方法,它实现了参数的绑定功能。
在参数绑定过程中,大致可以认为是这个过程:
Bind->MustBindWith->ShouldBindWith->b.Bind
在参数绑定中,无论是采用 c.Bind()
系列方法、或者是 c.ShouldBindWith()
系列方法,最终都是通过具体实例的 b.Bind()
方法来实现参数绑定到结构体指针。而这个实例可以在 binding
目录中找到其方法的实现文件。如: json.go
、 uri.go
以及 form.go
等等文件,文件名都对应着不同的 Content-Type
。
在 Gin
框架中下列方法可以用处理绑定:
// Bind 检查 Content-Type 来自动选择绑定引擎
// 依靠 "Content-Type" 头来使用不同的绑定
// "application/json" 绑定 JSON
// "application/xml" 绑定 XML
// 否则返回错误信息
// 如果 Content-Type ==“application / json”,JSON 或 XML 作为 JSON 输入,
// Bind 会将请求的主体解析为 JSON。
// 它将 JSON 有效负载解码为指定为指针的结构。
// 如果输入无效,它会写入 400 错误并在响应中设置 Content-Type 标题 “text / plain” 。
func (c *Context) Bind(obj interface{}) error
// BindJSON 是 c.MustBindWith(obj, binding.JSON) 的简写
func (c *Context) BindJSON(obj interface{}) error
// BindXML 是 c.MustBindWith(obj, binding.BindXML) 的简写
func (c *Context) BindXML(obj interface{}) error
// BindQuery 是 c.MustBindWith(obj, binding.Query) 的简写
func (c *Context) BindQuery(obj interface{}) error
// BindYAML 是 c.MustBindWith(obj, binding.YAML) 的简写
func (c *Context) BindYAML(obj interface{}) error
// BindHeader 是 c.MustBindWith(obj, binding.Header) 的简写
func (c *Context) BindHeader(obj interface{}) error
// BindUri 使用 binding.Uri 绑定传递的结构体指针。
// 如果发生任何错误,它将使用 HTTP 400 中止请求。
func (c *Context) BindUri(obj interface{}) error
// MustBindWith 使用指定的绑定引擎绑定传递的 struct 指针。
// 如果发生任何错误,它将使用 HTTP 400 中止请求。
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error
// ShouldBind 检查 Content-Type 来自动选择绑定引擎
// 依靠 "Content-Type" 头来使用不同的绑定
// "application/json" 绑定 JSON
// "application/xml" 绑定 XML
// 否则返回错误信息
// 如果 Content-Type ==“application/json” ,JSON 或 XML 作为 JSON 输入,
// Bind 会将请求的主体解析为JSON。
// 它将 JSON 有效负载解码为指定为指针的结构。
// 类似 c.Bind() ,但这个方法在 JSON 无效时不支持写 400 到响应里。
func (c *Context) ShouldBind(obj interface{}) error
// ShouldBindJSON 是c.ShouldBindWith(obj, binding.JSON)的简写
func (c *Context) ShouldBindJSON(obj interface{}) error
// ShouldBindXML 是c.ShouldBindWith(obj, binding.XML)的简写
func (c *Context) ShouldBindXML(obj interface{}) error
// ShouldBindQuery 是c.ShouldBindWith(obj, binding.Query)的简写
func (c *Context) ShouldBindQuery(obj interface{}) error
// ShouldBindYAML 是c.ShouldBindWith(obj, binding.YAML)的简写
func (c *Context) ShouldBindYAML(obj interface{}) error
// ShouldBindHeader 是c.ShouldBindWith(obj, binding.Header)的简写
func (c *Context) ShouldBindHeader(obj interface{}) error
// ShouldBindUri使用指定的绑定引擎绑定传递的struct指针。
func (c *Context) ShouldBindUri(obj interface{}) error
// ShouldBindWith使用自定的绑定引擎绑定传递的struct指针。
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error
// ShouldBindBodyWith与ShouldBindWith类似,但它存储请求
// ShouldBindBodyWith可进入上下文,并在再次调用时重用。
//
// 注意:此方法在绑定之前读取正文。 所以推荐使用
// 如果只需要调用一次,那么ShouldBindWith可以获得更好的性能。
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error)
1. 绑定查询字符串或表单数据
表单和 URLQuery
方式传递参数,程序通过绑定的方式得到参数值,在参数的提取上更加自动。
package main
import (
"log"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.POST("/testing", startPage)
route.Run(":8080")
}
func startPage(c *gin.Context) {
var person Person
// 如果是 `GET` 请求,只使用 `Form` 绑定引擎(`query`)。
// 如果是 `POST` 请求,首先检查 `content-type` 是否为 `JSON` 或 `XML`,
// 然后再使用 `Form`(`form-data`)。
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
程序运行在 Debug 模式时,在命令行运行下面三条命令:
curl -X POST "http://localhost:8080/testing?name=appleboy&address=xyz"
curl -H "Content-Type:application/json" -X POST -d '{"name":"appleeboy","address":"xyz"}' <http://localhost:8080/testing>
curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "name=appleboy&address=xyz" "<http://localhost:8080/testing>"
输出结果:
[GIN-debug] Listening and serving HTTP on :8080
2019/07/13 12:54:34 appleboy
2019/07/13 12:54:34 xyz
[GIN] 2019/07/13 - 12:54:34 | 200 | 18.9504ms | 127.0.0.1 | POST /testing?name=appleboy&address=xyz
2019/07/13 12:54:38 appleeboy
2019/07/13 12:54:38 xyz
[GIN] 2019/07/13 - 12:54:38 | 200 | 0s | 127.0.0.1 | POST /testing
2019/07/13 12:54:46 appleboy
2019/07/13 12:54:46 xyz
[GIN] 2019/07/13 - 12:54:46 | 200 | 0s | 127.0.0.1 | POST /testing
通过 POST
方法,采用 Urlencoded
编码或 JSON
方式都能被绑定正常解析。但如果把程序接收方法改为 GET
方法:
route.GET("/testing", startPage)
则只能通过 URL Query 传递参数:
curl -X GET "http://localhost:8080/testing?name=appleboy&address=xyz"
这样通过 URL Query
传递参数也能被正常绑定。
2. Multipart/Urlencoded 绑定
通过表单传递参数,下面程序通过绑定的方式得到参数值。
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
// 可显式绑定表单
// c.ShouldBindWith(&form, binding.Form)
// 或者简单地使用 ShouldBind 方法自动绑定
if c.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
router.Run(":8080")
}
上面程序中结构体的标签: form:"user"
,表示在 form
表单中的名为 user
。
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
程序运行在 Debug 模式时,在命令行运行下面两条命令:
curl -X POST -d "user=user&password=password" <http://localhost:8080/login>
Curl -H "Content-Type:multipart/form-data" -X POST -d "user=user&password=password" http://localhost:8080/login
3. URI 参数绑定
Gin
框架支持在路由 URI
中存在参数,也支持通过绑定得到这些参数,需要在结构体中指定字段标签为 uri
。
package main
import "github.com/gin-gonic/gin"
type Person struct {
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
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")
}
UserID string `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
程序运行在 Debug 模式时,在命令行运行下面命令:
curl -X GET http://localhost:8080/Go/42
4. 绑定 HTML 复选框
Gin
框架很方便地通过绑定得到 HTML FORM
元素的值,需要在结构体中指定字段标签form:filedname
。
type CheckForm struct {
Colors []string `form:"colors[]"`
}
func main() {
router := gin.Default()
router.Static("/", "./public")
router.POST("/check", func(c *gin.Context) {
var form CheckForm
// 简单地使用 ShouldBind 方法自动绑定
if c.ShouldBind(&form) == nil {
c.JSON(200, gin.H{"color": form.Colors})
}
})
router.Run(":8080")
}
index.html 文件放在程序目录下 public 目录中。
<form action="/check" 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>
注意,上面程序中结构体标签: colors[]
与复选框的名字一致,这里表示数组所以可以得到多个已选项的值。
运行程序,通过浏览器访问 http://localhost:8080/ ,出现复选框表单,选择两个以上选项,这里选择红,绿两种颜色,然后提交表单(请求发送到 http://localhost:8080/check )。
页面显示,符合提交的选项:
{"color":["red","green"]}
- 1
5. 绑定表单数据至嵌入结构体
前面已经知道通过绑定可以自动取得数据到简单结构体对象,对有嵌入的结构体也可以通过绑定自动得到数据,不过嵌入的结构体后面不要指定标签。
type StructA struct {
FieldA string `form:"field_a"`
}
type StructB struct {
NestedStruct StructA // 不要指定标签
FieldB string `form:"field_b"`
}
type StructC struct {
NestedStructPointer *StructA
FieldC string `form:"field_c"`
}
type StructD struct {
NestedAnonyStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}
func GetDataB(c *gin.Context) {
var b StructB
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStruct,
"b": b.FieldB,
})
}
func GetDataC(c *gin.Context) {
var b StructC
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStructPointer,
"c": b.FieldC,
})
}
func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonyStruct,
"d": b.FieldD,
})
}
func main() {
router := gin.Default()
router.GET("/getb", GetDataB)
router.GET("/getc", GetDataC)
router.GET("/getd", GetDataD)
router.Run()
}
输入输出结果:
curl "http://localhost:8080/getb?field_a=hello&field_b=world"
Go{"a":{"FieldA":"hello"},"b":"world"}
curl "http://localhost:8080/getc?field_a=hello&field_c=world"
Go{"a":{"FieldA":"hello"},"c":"world"}
curl "http://localhost:8080/getd?field_x=hello&field_d=world"
Go{"d":"world","x":{"FieldX":"hello"}}
6. 将请求体绑定到不同的结构体中
一般通过调用 ShouldBind()
方法绑定数据,但注意某些情况不能多次调用这个方法。
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}
type formB struct {
Bar string `json:"bar" xml:"bar" binding:"required"`
}
func BindHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// c.ShouldBind 使用了 c.Request.Body ,不可重用。
if errA := c.ShouldBind(&objA); errA != nil {
fmt.Println(errA)
c.String(http.StatusOK, `the body should be formA`)
// 因为现在 c.Request.Body 是 EOF,所以这里会报错。
} else if errB := c.ShouldBind(&objB); errB != nil {
fmt.Println(errB)
c.String(http.StatusOK, `the body should be formB`)
} else {
c.String(http.StatusOK, `Success`)
}
}
func main() {
route := gin.Default()
route.Any("/bind", BindHandler)
route.Run(":8080")
}
运行程序,通过浏览器访问 http://localhost:8080/bind?foo=foo&bar=bar ,页面显示:
the body should be formA
程序运行在 Debug 模式时,在命令行运行下面命令:
curl -H "Content-Type:application/json" -v -X POST -d '{"foo":"foo","bar":"bar"}' http://localhost:8080/bind
- 1
命令返回:
the body should be formB
表明在第二次运行 ShouldBind()
方法时出错,要想多次绑定,可以使用 c.ShouldBindBodyWith()
方法。
func BindHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// ShouldBindBodyWith() 读取 c.Request.Body 并将结果存入上下文。
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA != nil {
fmt.Println(errA)
c.String(http.StatusOK, `the body should be formA`)
// 这时, 复用存储在上下文中的 body 。
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB != nil {
fmt.Println(errB)
c.String(http.StatusOK, `the body should be formB JSON`)
// 可以接受其他格式
} else {
c.String(http.StatusOK, `Success`)
}
}
c.ShouldBindBodyWith()
会在绑定之前将请求体存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。
只有某些格式需要此功能,如 JSON
,XML
,MsgPack
,ProtoBuf
。对于其他格式,如 Query
,Form
,FormPost
,FormMultipart
可以多次调用 c.ShouldBind()
而不会造成任任何性能损失,这也是前面结构体中的标签没有定义 form
,只有定义 json:"foo" xml:"foo" binding:"required"
的原因。
7. 只绑定 URL Query 参数
ShouldBind()
方法支持 URL Query
参数绑定,也支持 POST
参数绑定。而 ShouldBindQuery()
方法只绑定 URL Query
参数而忽略 POST
数据。
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
fmt.Println(person.Name)
fmt.Println(person.Address)
c.String(200, "Success")
} else {
c.String(400, "Error")
}
}
func main() {
route := gin.Default()
route.Any("/bindquery", startPage)
route.Run(":8080")
}
运行程序,通过浏览器访问 http://localhost:8080/ ,页面显示 “Sucess” 。输出结果:
[GIN-debug] GET /bindquery --> main.startPage (3 handlers)
[GIN-debug] POST /bindquery --> main.startPage (3 handlers)
[GIN-debug] PUT /bindquery --> main.startPage (3 handlers)
[GIN-debug] PATCH /bindquery --> main.startPage (3 handlers)
[GIN-debug] HEAD /bindquery --> main.startPage (3 handlers)
[GIN-debug] OPTIONS /bindquery --> main.startPage (3 handlers)
[GIN-debug] DELETE /bindquery --> main.startPage (3 handlers)
[GIN-debug] CONNECT /bindquery --> main.startPage (3 handlers)
[GIN-debug] TRACE /bindquery --> main.startPage (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
titan
cs
[GIN] 2019/07/13 - 17:06:23 | 200 | 0s | ::1 | GET /bindquery?name=titan&address=cs
输出表明 URL Query
参数通过 GET
方法能被程序正常绑定,注意上面程序中使用了 Any()
方法,它能匹配众多的 HTTP
方法。
如果程序继续运行在 Debug 模式时,在命令行运行下面命令:
curl -v -X POST -d "name=titan&address=cs" http://localhost:8080/bindquery
* Connected to localhost (::1) port 8080 (#0)
> POST /bindquery HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Length: 21
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 21 out of 21 bytes
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
< Date: Sat, 13 Jul 2019 17:12:37
< Content-Length: 7
<
Success
命令行的返回表明通过 POST
方法已经成功提交请求,服务端成功返回,状态代码: 200 ,返回内容: Success 。
控制台输出结果:
[GIN] 2019/07/13 - 17:12:37 | 200 | 0s | ::1 | POST /bindquery
- 1
从控制台输出可以看到,通过 POST
提交的数据没有正常绑定。但是前面通过 ShouldBind()
方法可以正常绑定。这表明 ShouldBindQuery()
只绑定 URL Query
参数而忽略 POST
数据。
8. JSON 模型绑定
通过 POST
方法提交 JSON
格式数据,程序通过绑定的方式得到 JSON
数据,并传递给结构体,但需要指定字段标签为 json
。
// 绑定 JSON
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
// 绑定 JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// 绑定 HTML 表单 (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// 根据 Content-Type Header 推断使用哪个绑定器。
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// 监听并启动服务
router.Run(":8080")
}
输出结果:
curl -v -H 'content-type: application/json' -X POST http://localhost:8080/loginJSON -d '{ "user": "manu" , "password" :"123" }'
> POST /loginJSON HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> content-type: application/json
> Content-Length: 38
>
* upload completely sent off: 38 out of 38 bytes
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 13 Jul 2019 15:08:29 GMT
< Content-Length: 31
<
{"status":"you are logged in"}
9. Header 头信息绑定
Header
也可以传递参数,程序通过绑定的方式得到参数值,在结构体的字段标签上需要指定为 header
。
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
router.Run(":8080")
}
运行命令
curl -H "rate:300" -H "domain:music" http://localhost:8080/
{"Domain":"music","Rate":300}
通过 curl
命令带上自定义的头部信息给 Handler
处理程序, ShouldBindHeader()
方法自动绑定头部变量到结构体。
参考:https://gitbook.cn/gitchat/column/5dab061e7d66831b22aa0b44/topic/5dab09f37d66831b22aa0b5d