Gin 框架学习笔记 — 参数自动绑定到结构体

参数绑定模型可以将请求体自动绑定到结构体中,目前支持绑定的请求类型有 JSONXMLYAML 和标准表单 form数据 foo=bar&boo=baz 等。换句话说,只要定义好结构体,就可以将请求中包含的数据自动接收过来,这是 Gin 框架非常神奇的功能。

在定义绑定对应的结构体时,需要给结构体字段设置绑定类型的标签,比如绑定 JSON 数据时,设置字段标签为 json:"fieldname" 。使用绑定可以更快捷地把数据传递给程序中的数据对象。

使用 Gin框架中系列绑定有关方法时,Gin 会根据请求头中 Content-Type 推断如何绑定,也就是自动绑定。但如果明确绑定的类型,开发人员也可以使用 MustBindWith() 方法或 BindJSON() 等方法而不用自动推断。可以指定结构体某字段是必需的,字段需要设置标签 binding:"required" ,但如果绑定时是空值,Gin 会报错。

Gin 框架的 binding 包中,定义了 Content-Type 请求头信息的多种 MIME 类型,以便在自动绑定时进行类型判别进而采用对应的处理方法:

1
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 对象。一共可以实例化为下面几种类型:

1
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) ,这里的 bjsonBinding{} 对象。其他像 XML 等的处理过程类似。

c.MustBindWith() 方法会统一调用 c.ShouldBindWith() 方法,在 c.ShouldBindWith() 方法中会调用具体实例的处理方法: b.Bind(c.Request, obj) ,这个 b.Bind()方法很关键,每种 binding 实例对象都有实现这个方法,它实现了参数的绑定功能。

在参数绑定过程中,大致可以认为是这个过程:

1
Bind->MustBindWith->ShouldBindWith->b.Bind

在参数绑定中,无论是采用 c.Bind() 系列方法、或者是 c.ShouldBindWith() 系列方法,最终都是通过具体实例的 b.Bind() 方法来实现参数绑定到结构体指针。而这个实例可以在 binding 目录中找到其方法的实现文件。如: json.gouri.go 以及 form.go 等等文件,文件名都对应着不同的 Content-Type

Gin 框架中下列方法可以用处理绑定:

1
// 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 方式传递参数,程序通过绑定的方式得到参数值,在参数的提取上更加自动。

1
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 模式时,在命令行运行下面三条命令:

1
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>"

输出结果:

1
[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 方法:

1
route.GET("/testing", startPage)

则只能通过 URL Query 传递参数:

1
curl -X GET "http://localhost:8080/testing?name=appleboy&address=xyz"

这样通过 URL Query 传递参数也能被正常绑定。

2. Multipart/Urlencoded 绑定

通过表单传递参数,下面程序通过绑定的方式得到参数值。

1
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

1
User string `form:"user" binding:"required"` Password string `form:"password" binding:"required"`

程序运行在 Debug 模式时,在命令行运行下面两条命令:

1
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

1
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") }

上面程序中结构体的标签: uri:"id" ,表示在 URI 中的参数名为 id

1
UserID string `uri:"id" binding:"required"` Name string `uri:"name" binding:"required"`

程序运行在 Debug 模式时,在命令行运行下面命令:

1
curl -X GET http://localhost:8080/Go/42

4. 绑定 HTML 复选框

Gin 框架很方便地通过绑定得到 HTML FORM 元素的值,需要在结构体中指定字段标签form:filedname

1
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 目录中。

1
<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 )。

页面显示,符合提交的选项:

1
{"color":["red","green"]}

5. 绑定表单数据至嵌入结构体

前面已经知道通过绑定可以自动取得数据到简单结构体对象,对有嵌入的结构体也可以通过绑定自动得到数据,不过嵌入的结构体后面不要指定标签。

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

输入输出结果:

1
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() 方法绑定数据,但注意某些情况不能多次调用这个方法。

1
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 模式时,在命令行运行下面命令:

1
curl -H "Content-Type:application/json" -v -X POST -d '{"foo":"foo","bar":"bar"}' http://localhost:8080/bind

命令返回:
the body should be formB
表明在第二次运行 ShouldBind() 方法时出错,要想多次绑定,可以使用 c.ShouldBindBodyWith() 方法。

1
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() 会在绑定之前将请求体存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。
只有某些格式需要此功能,如 JSONXMLMsgPackProtoBuf 。对于其他格式,如 QueryFormFormPostFormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失,这也是前面结构体中的标签没有定义 form ,只有定义 json:"foo" xml:"foo" binding:"required" 的原因。

7. 只绑定 URL Query 参数

ShouldBind() 方法支持 URL Query 参数绑定,也支持 POST 参数绑定。而 ShouldBindQuery() 方法只绑定 URL Query 参数而忽略 POST 数据。

1
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” 。输出结果:

1
[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 模式时,在命令行运行下面命令:

1
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 。
控制台输出结果:

1
[GIN] 2019/07/13 - 17:12:37 | 200 | 0s | ::1 | POST /bindquery

从控制台输出可以看到,通过 POST 提交的数据没有正常绑定。但是前面通过 ShouldBind() 方法可以正常绑定。这表明 ShouldBindQuery() 只绑定 URL Query 参数而忽略 POST 数据。

8. JSON 模型绑定

通过 POST 方法提交 JSON 格式数据,程序通过绑定的方式得到 JSON 数据,并传递给结构体,但需要指定字段标签为 json

1
// 绑定 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") }

输出结果:

1
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

1
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") }

运行命令

1
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