【Golang】Gin 框架之请求参数绑定

最近在用Gin来做一个side project,用于练手以及学习前端。看了Gin的文档,此文只是将相关的文档作为一个归类,留存起来。首先我们看看,Gin中模型绑定和校验,是其他绑定类型请求的基础;后面再分别介绍Gin中相关的绑定类型(见下面的表)。

类型 重要程度
绑定Url 重要
请求参数与自定义结构绑定 重要
请求参数是前端上送的CheckBox 重要
仅仅绑定查询 一般,特殊化
绑定Header 一般
绑定查询类型或者POST数据 重要

模型绑定和校

模型绑定的作用是将请求体绑定到自定义类型,目前Gin支持:JSON、XML、YAML和标准form请求参数(比如:foo=bar&boo=baz)。Gin使用go-payground/validator/validtor/v10,做参数的校验。若参数是必输,可以说明的使用 binding:"required"修饰之。当在绑定的时候发现是空值就会返回错误。所以语法格式:

`绑定标签类型:"fieldname" binding:"required"`

Gin提供两种类型的方法来实现绑定功能,并且在调用绑定方法的时候,会根据请求中头部Content-Type内容来调用相关的方法。如果你确认绑定的参数类型,可以直接使用MustBindWithShouldBindWith,否则请使用ShouldBind作为万能钥匙。下面具体看一下此两种类型:

类型 功能 方法 注意点
Must bind 调用钩子函数:MustindWith;绑定出现错误程序中断:c.AbortWithError(400, err).SetType(ErrorTypeBind),效果就是返回400Content-Type:text.plain;charset=utf-8 Bind, BindJSON, BindXML, BindQuery, BindYAML, BindHeader 不够灵活
Should bind 调用钩子函数:ShouldBindWith ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindHeader 灵活,绑定错误需要客户自行处理

示例代码

package main

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

// Binding from JSON、form、xml
type Login struct {
    User string `form:"user" json:"user" xml:"user" binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"required"`
}


func loginFormHandler(c *gin.Context) {
    var form Login
    // This will infer what binder to use depending on the 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"})
}

// Example for binding JSON ({"user": "manu", "password": "123"})
func loginJSONHandler(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",
    })
}

// Example for binding XML (
//  <?xml version="1.0" encoding="UTF-8"?>
//  <root>
//      <user>user</user>
//      <password>123</password>
//  </root>)
func loginXMLHandler(c *gin.Context) {
    var xml Login
    if err := c.ShouldBindXML(&xml); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    if xml.User != "manu" || xml.Password != "123" {
        c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
}
func main() {
    router := gin.Default()
    router.POST("/loginJSON", loginJSONHandler)
    // Example for binding a HTML form (user=manu&password=123)
    router.POST("/loginForm", loginFormHandler)
    router.POST("/loginXML", loginXMLHandler)

    router.Run()
}

测试

类型 测试命令
JSON curl -X POST 'http://localhost:8080/loginJSON' -v -d '{"user":"manu", "password":"123"}'
XML curl -X POST "http://localhost:8080/loginXML" -v -d '<?xml version="1.0" encoding="UTF-8"?><root><user>manu</user><password>123</password></root>'
form curl -X POST "http://localhost:8080/loginForm" -v -d 'user=manu&password=123'

绑定Url

示例代码

此类型主要用在RESTful类型的接口,具体的示例代码如下:

package main
// 绑定Uri
import "github.com/gin-gonic/gin"

func main() {
    route := gin.Default()
    route.GET("/:name/:id", func (c *gin.Context) {
        c.JSON(200, gin.H{
        "name": c.Param("name"),
        "uuid": c.Param("id"),
        })
    })
    route.Run()
}

测试

curl -X GET 'http://localhost:8080/yunfeng/12345'
{"name":"yunfeng","uuid":"12345"}

请求参数是前端上送的CheckBox

示例代码

我们这里需要用到Gin一个静态资源绑定功能或者加载HTML功能。

package main

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

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

func formHandler(c *gin.Context) {
    var fakeForm myForm
    // If `GET`, only `Form` binding engine (`query`) used.
    // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`)
    err := c.ShouldBind(&fakeForm)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error":err,
        })
    }
    c.JSON(http.StatusOK, gin.H{
        "color": fakeForm.Colors,
    })
}

func indexHandler(c *gin.Context) {
    // render form.html
    c.HTML(http.StatusOK, "form.html", nil)
}

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

    // load html 
    router.LoadHTMLGlob("static/*")
    router.Static("/static", "./static")
    router.GET("/", indexHandler)
    router.POST("/testBindHtmlCheckboxes", formHandler)
    router.Run()
}

测试

  1. 在命令行中
    go build -o bindHtml & ./bindHtml

  2. 在浏览器中输入
    http://localhost:8080/static/form.html 或者 http://localhost:8080

bind-html-checkbox.png
  1. 提交之后的结果:
  • 浏览器上面的变为地址
    http://localhost:8080/testBindHtmlCheckboxes
  • 展示的信息
    {"color":["red","green"]}

请求参数与自定义结构绑定

示例代码

package main

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

// StructA 普通结构体
type StructA struct {
    FieldA string `form:"field_a"`
}

// StructB 嵌套型结构体
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(fib)
    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() {
    r := gin.Default()
    r.GET("/getb", GetDataB)
    r.GET("/getc", GetDataC)
    r.GET("/getd", GetDataD)

    r.Run()
}
curl "http://localhost:8080/getb?field_a=hello&field_b=world"
{"a":{"FieldA":"hello"},"b":"world"}

curl "http://localhost:8080/getc?field_a=hello&field_c=worldc&filed_x=helloX&filed_d=wordd"
{"a":{"FieldA":"hello"},"c":"worldc"}

curl "http://localhost:8080/getd?field_a=hello&field_c=worldc&field_x=helloX&field_d=wordd"
{"d":"wordd","x":{"FieldX":"helloX"}}

仅仅绑定查询

示例代码

package main

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

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

func onlyBindQueryStringHandler(c *gin.Context) {
    var person Person
    if err := c.BindQuery(&person); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "Name":person.Name,
        "address":person.Address,
    })
}

func main() {
    router := gin.Default()
    router.Any("/testOnlyBindQuery", onlyBindQueryStringHandler)
    router.Run()
}

测试

  • 测试GET请求

curl -X GET "localhost:8080/testOnlyBindQuery?name=yunfen&address=xyz"

{"Name":"yunfen","address":"xyz"}

  • 测试POST请求,发现请求体的数据忽略了

curl -X POST "localhost:8080/testOnlyBindQuery?name=yunfen&address=xyz" -d "name=hhetest&address=ffff"

{"Name":"yunfen","address":"xyz"}

绑定查询类型或者POST数据

示例代码

package main

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

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

func bindQueryStringOrPostDataHandler(c *gin.Context) {
    var person Person
    if err := c.ShouldBind(&person); err != nil {
        c.JSON(http.StatusBadRequest, gin.H {
            "error":err.Error(),
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "Name": person.Name,
        "Address": person.Address,
        "Birthday": person.Birthday,
        "CreateTime": person.CreateTime,
        "UnixTime": person.UnixTime,
    })
}

func main() {
    router := gin.Default()
    router.Any("/testQueryAndPost", bindQueryStringOrPostDataHandler)
    router.Run()
}

测试

  • 测试GET请求

curl -X GET "localhost:8080/testQueryAndPost?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"

{"Address":"xyz","Birthday":"1992-03-15T00:00:00Z","CreateTime":"2019-07-06T16:00:33.000000123+08:00","Name":"appleboy","UnixTime":"2019-07-06T16:00:33+08:00"}

  • 测试POST请求

curl -X POST "localhost:8080/testQueryAndPost" -d "name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"

{"Address":"xyz","Birthday":"1992-03-15T00:00:00Z","CreateTime":"2019-07-06T16:00:33.000000123+08:00","Name":"appleboy","UnixTime":"2019-07-06T16:00:33+08:00"}

绑定Header

示例代码

package main

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

type DomainRate struct {
    Rate int `header:"Rate"`
    Domain string `header:"Domain"`
}

func bindHeaderHandler(c *gin.Context) {
    var header DomainRate
    if err := c.ShouldBindHeader(&header); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "status":err.Error(),
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "Rate":header.Rate,
        "Domain":header.Domain,
    })
}

// bind header
func main() {
    router := gin.Default()
    router.GET("/testBindHeader", bindHeaderHandler)
    router.Run()
}

测试

curl -X GET "localhost:8080/testBindHeader" -H "rate":100 -H "domain":localhost

{"Domain":"localhost","Rate":100}

将请求body绑定到不同的结构

JSONXMLMsgPackProtoBuf等格式请求体绑定,ShouldBind或者ShouldBindWith消费的
c.Request.Body,会导致 c.Request.Body变成EOF。为此,ShouldBindBodyWith会在绑定之前
将请求体保存上下文中,但多少带来一定的性能损耗。若确定只绑定一次,就不要此方法。而其他格式的比如:QueryFormFormPostFormMultipart在多次使用ShouldBind绑定并不会消耗性能。

// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
// body into the context, and reuse when it is called again.
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.

例子

package main

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

type formA struct {
    Foo string `json:"foo"  binding:"required"`
}

type formB struct {
    Bar string `json:"bar" binding:"required"`
}

func usingShoudBindHandler(c *gin.Context) {
    objA := formA{}
    objB := formB{}
    // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
    if errA := c.ShouldBindWith(&objA, binding.JSON); errA != nil {
        c.String(http.StatusBadRequest, "bind A JSON err:%s\n", errA.Error())
    } else if errB := c.ShouldBindWith(&objB, binding.JSON); errB != nil {
        // Always an error is occurred by this because c.Request.Body is EOF now.
        c.String(http.StatusBadRequest, "bind B JSON err:%s\n", errB.Error())
    }  else {
        c.String(http.StatusOK, "Foo:%s,Bar:%s\n", objA.Foo, objB.Bar)
    }
}


func usingShoudBindBodyWithHandler(c *gin.Context) {
    objA := formA{}
    objB := formB{}
    // This reads c.Request.Body and stores the result into the context.
    if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA != nil {
        c.String(http.StatusBadRequest, "bind A JSON err:%s\n", errA.Error())
    } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB != nil {
        // At this time, it reuses body stored in the context.
        c.String(http.StatusBadRequest, "bind B JSON err:%s\n", errB.Error())
    }  else {
        c.String(http.StatusOK, "Foo:%s,Bar:%s\n", objA.Foo, objB.Bar)
    }
}

func main() {
    router := gin.Default()
    router.POST("/testBodyDiffStr1", usingShoudBindHandler)
    router.POST("/testBodyDiffStr2", usingShoudBindBodyWithHandler)

    router.Run()
}

测试

curl -X POST "localhost:8080/testBodyDiffStr1" -d '{"foo":"zdf", "bar":"test1"}'

bind B JSON err:EOF

curl -X POST "localhost:8080/testBodyDiffStr2" -d '{"foo":"zdf", "bar":"test1"}'

Foo:zdf,Bar:test1

参考资料

go-gin doc
go-gin readme

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容

  • Nginx 的配置文件使用的就是一门微型的编程语言,许多真实世界里的 Nginx 配置文件其实就是一个一个的小程序...
    SkTj阅读 4,195评论 0 7
  • 转发自:http://shanshanpt.github.io/2016/05/03/go-gin.html gi...
    dncmn阅读 6,047评论 0 1
  • 所谓框架 框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写...
    人世间阅读 216,210评论 11 242
  • 大多数 Nginx 新手都会频繁遇到这样一个困惑,那就是当同一个location配置块使用了多个 Nginx 模块...
    SkTj阅读 7,692评论 0 12
  • 《剎那》 文/【貓影&鈺雯】 剎那 光之耀 激盪 併發出的炙烈 燃點是你的靈魂 那道光 我的整個宇宙 20160711晚
    猫影钰雯阅读 237评论 0 3