Gin 笔记(04)— URLQuery参数处理、Bind URI、Bind POST 到指定结构体、POST 表单处理

潘凯
2023-12-01

1. URLQuery参数处理

Gin 框架中下列方法可以用处理 URLQuery 参数:

    // 返回指定名字参数的值,c.Params.ByName(key) 简写,
    // 如: "/user/:id",则返回 id := c.Param("id")  id == "john"
    func (c *Context) Param(key string) string

    // Query 返回 query 中指定参数的值,如不存在则返回""。
    // c.Request.URL.Query().Get(key) 的简写,
    // 如 GET /path?id=1234&name=Manu&value=,则 c.Query("id") == "1234"
    func (c *Context) Query(key string) string

    // DefaultQuery 返回 query 中指定参数的值,如不存在则返回指定的值 defaultValue。
    // GET /?name=Manu&lastname=
    // c.DefaultQuery("name", "unknown") == "Manu"
    // c.DefaultQuery("id", "none") == "none"
    // c.DefaultQuery("lastname", "none") == ""
    func (c *Context) DefaultQuery(key, defaultValue string) string

    // GetQuery 类似 Query() , 返回 query 中指定参数的值,如参数存在(即使值为"")
    // 则返回 (value, true),不存在的参数则返回指定的值 ("", false)。
    // c.Request.URL.Query().Get(key) 的简写
    //     GET /?name=Manu&lastname=
    //     ("Manu", true) == c.GetQuery("name")
    //     ("", false) == c.GetQuery("id")
    //     ("", true) == c.GetQuery("lastname")
    func (c *Context) GetQuery(key string) (string, bool)

    // 返回 URL 指定名字参数的字符串切片,切片的长度与指定参数的值多少有关
    func (c *Context) QueryArray(key string) []string

    //  返回 URL 指定名字参数的字符串切片与布尔值,值存在则为 true
    func (c *Context) GetQueryArray(key string) ([]string, bool)

    // 返回 URL 指定名字参数的字符串字典
    func (c *Context) QueryMap(key string) map[string]string

    // 返回 URL 指定名字参数的字符串字典与布尔值,值存在则为 true
    func (c *Context) GetQueryMap(key string) (map[string]string, bool)

1.1 Query 查询字符串参数

对于类似 /welcome?firstname=Jane&lastname=Doe 这样的 URL? 后面为 Query 查询字符串参数,在 Gin 框架中有专门方法来处理这些参数,例如:

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

        // 使用现有的基础请求对象解析查询字符串参数。
        // 示例 URL: /welcome?firstname=Jane&lastname=Doe
        router.GET("/welcome", func(c *gin.Context) {
             firstname := c.DefaultQuery("firstname", "Guest")
            lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的快捷方式
            name, _ := c.GetQuery("lastname")

            c.String(http.StatusOK, "Hello %s %s %s", firstname, lastname, name)
        })
        router.Run(":8080")
    }

程序运行在 Debug 模式时,通过浏览器访问

http://localhost:8080/welcome?firstname=Jane&lastname=Doe

上面是通过 Query 方式传递参数,在 Gin 框架中可以通过 Query()DefaultQuery()GetQuery() 等方法得到指定参数的值。

Query() 读取 URL 中的地址参数,例如

 // GET /path?id=1234&name=Manu&value=
   c.Query("id") == "1234"
   c.Query("name") == "Manu"
   c.Query("value") == ""
   c.Query("wtf") == ""

DefaultQuery():类似 Query(),但是如果 key 不存在,会返回默认值

 //GET /?name=Manu&lastname=
 c.DefaultQuery("name", "unknown") == "Manu"
 c.DefaultQuery("id", "none") == "none"
 c.DefaultQuery("lastname", "none") == ""

输出结果:

$ curl -X GET  http://localhost:8080/welcome?firstname=wohu\&lastname='1104'
Hello wohu 1104

$ curl -X GET  "http://localhost:8080/welcome?firstname=wohu&lastname=1104"
Hello wohu 1104

1.2 URI 路由参数

对于类似 /user/:firstname/:lastname:lastnameGin 框架中路由参数的一种写法,表示 lastname 为任意的字符串,访问时使用具体值。

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

    router.GET("/user/:firstname/:lastname", func(c *gin.Context) {
        fname := c.Param("firstname")
        lname := c.Param("lastname")

        c.String(http.StatusOK, "Hello %s %s ", fname, lname)
    })

    router.Run(":8080")
}

Param() 方法能快速返回路由 URI 指定名字参数的值,它是 c.Params.ByName(key) 方法的简写。如路由定义为: "/user/:id",则返回 id := c.Param("id")

程序运行在 Debug 模式时,通过浏览器访问

http://localhost:8080/user/wohu/1104

输出:

hello wohu 1104
func main() {
	router := gin.Default()

	// This handler will match /user/john but will not match /user/ or /user
	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")	// name == "john"
		c.String(http.StatusOK, "Hello %s", name)
	})

	// However, this one will match /user/john/ and also /user/john/send
	// If no other routers match /user/john, it will redirect to /user/john/
	router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
		c.String(http.StatusOK, message)
	})

	router.Run(":8080")
}

上面代码路由路径中带参数的方式有 :*两种,不同符号代表不同含义,通过 Param() 方法取得对应的字符串值。

  • :表示参数值不为空,且不以 /结尾;
  • *表示参数可为空,可为任意字符包括 /

Param() 方法能快速返回路由 URI 指定名字参数的值,它是 c.Params.ByName(key) 方法的简写。如路由定义为: “/user/:id”,则返回 id := c.Param("id")

1.3 URL Query 字符串参数或表单参数映射到字典

Gin 框架中 PostFormMap()QueryMap() 等方法在某些情况下非常有用,下面对参数映射到字典做了简单说明,

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

	router.POST("/post", func(c *gin.Context) {

		ids := c.QueryMap("ids")
		names := c.PostFormMap("names")

		fmt.Printf("ids: %v; names: %v", ids, names)
	})
	router.Run(":8080")
}

查询

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou
    curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "names[first]=thinkerou&names[second]=tianou" -g "http://localhost:8080/post?ids[a]=1234&ids[b]=hello"

输出:

ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]

2. Binding 绑定

Bind():将消息体作为指定的格式解析到 Go struct 变量中。而绑定(Binding)是通过一系列方法可以将请求体中参数自动绑定到自定义的结构体中,从而可以简单快速地得到对应的参数值。

2.1 绑定 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() {
	r := gin.Default()
	r.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})
	})
	r.Run(":8080")
}

gin.H 可当做为字典类型,在 utils.go 文件中定义如下:

type H map[string]interface{}

输出结果:

$ curl localhost:8080/wohu/987fbc97-4bed-5078-9f07-9141ba07c9f3
{"name":"wohu","uuid":"987fbc97-4bed-5078-9f07-9141ba07c9f3"}


$ curl localhost:8080/wohu/uuid
{"msg":[{}]}

2.2 只绑定查询字符串

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

func main() {
	route := gin.Default()
	route.Any("/testing", startPage)
	route.Run(":8085")
}

func startPage(c *gin.Context) {
	var person Person
	if c.ShouldBindQuery(&person) == nil {
		log.Println("====== Only Bind By Query String ======")
		log.Println(person.Name)
		log.Println(person.Address)
	}
	c.String(200, "Success")
}

2.3 自定义结构体绑定表单数据请求

package main

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

type StructA struct {
	FieldA string `form:"field_a"`
}

type StructB struct {
	StructAValue StructA
	FieldB       string `form:"field_b"`
}

type StructC struct {
	StructAPointer *StructA
	FieldC         string `form:"field_c"`
}

type StructD struct {
	AnonyStruct 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.StructAValue,
		"b": b.FieldB,
	})
}

func GetDataC(c *gin.Context) {
	var cStruct StructC
	c.Bind(&cStruct)
	c.JSON(200, gin.H{
		"a": cStruct.StructAPointer,
		"c": cStruct.FieldC,
	})
}

func GetDataD(c *gin.Context) {
	var d StructD
	c.Bind(&d)
	c.JSON(200, gin.H{
		"x": d.AnonyStruct,
		"d": d.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=world"
{"a":{"FieldA":"hello"},"c":"world"}

$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}

2.4 绑定查询字符串或POST数据

package main

import (
	"log"
	"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() {
	r := gin.Default()
	r.GET("/testing", startPage)
	r.Run()
}

func startPage(c *gin.Context) {
	var person Person
	// If `GET`, only `Form` binding engine (`query`) used.
	// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
	// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
	if c.ShouldBind(&person) == nil {
		log.Println(person.Name)
		log.Println(person.Address)
		log.Println(person.Birthday)
	}

	c.String(200, "Success")
}

输出结果:

$ curl -X GET "localhost:8080/testing?name=wohu&address=city&birthday=1992-03-15"
Success

2.5 绑定请求消息体到不同结构体

绑定请求体的使用方法 c.Request.Body,但是它不能被多次调用。

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

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

func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
  if errA := c.ShouldBind(&objA); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Always an error is occurred by this because c.Request.Body is EOF now.
  } else if errB := c.ShouldBind(&objB); errB == nil {
    c.String(http.StatusOK, `the body should be formB`)
  } else {
    ...
  }
}

应该使用 c.ShouldBindBodyWith 避免该错误

func SomeHandler(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.StatusOK, `the body should be formA`)
  // At this time, it reuses body stored in the context.
  } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // And it can accepts other formats
  } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  } else {
    ...
  }
}
  • c.ShouldBindBodyWith在绑定前将 body存储到上下文中。这对性能有轻微的影响,所以如果你足以一次性调用绑定,你不应该使用这个方法。
  • 这个功能只需要用于某些格式  JSON, XML, MsgPack, ProtoBuf。对于其他格式,QueryFormFormPostFormMultipart,可以通过 c.ShouldBind()多次调用而不会对性能造成任何损害。

2.6 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) {
		// you can bind multipart form with explicit binding declaration:
		// c.ShouldBindWith(&form, binding.Form)
		// or you can simply use autobinding with ShouldBind method:
		var form LoginForm
		// in this case proper binding will be automatically selected
		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")
}

输出结果:

$ curl --form user=user --form password=password http://localhost:8080/login
{"status":"you are logged in"}

2.7 Multipart/Urlencoded 表单

表单提交方法为 POST时,enctype 属性为 application/x-www-form-urlencodedmultipart/form-data 的差异:

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

	router.POST("/form_post", func(c *gin.Context) {
		message := c.PostForm("message")
		nick := c.DefaultPostForm("nick", "anonymous")

		c.JSON(200, gin.H{
			"status":  "posted",
			"message": message,
			"nick":    nick,
		})
	})
	router.Run(":8080")
}

输出结果:
可以看到在简单的键值对传递时,属性为 application/x-www-form-urlencodedmultipart/form-data 基本不存在差异。都能正常返回 JSON

curl -H "Content-Type:multipart/form-data" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"

curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"

$ curl -X POST   --form message=message --form nick=nick http://localhost:8080/form_post 
{"message":"message","nick":"nick","status":"posted"}

3. POST 表单处理

<form> 中,enctype 属性规定当表单数据提交到服务器时如何编码(仅适用于 method="post" 的表单)。formenctype 属性是 HTML5 中的新属性,formenctype 属性覆盖 <form>元素的 enctype 属性。

常用有两种:application/x-www-form-urlencodedmultipart/form-data,默认为 application/x-www-form-urlencoded

当表单提交方法为 GET 时,浏览器用 x-www-form-urlencoded 的编码方式把表单数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串追加到 URL 后面。

当表单提交方法为 POST 时,浏览器把表单数据封装到请求体中,然后发送到服务端。如果此时 enctype 属性为 application/x-www-form-urlencoded,则请求体是简单的键值对连接,格式如下:k1=v1&k2=v2&k3=v3。而如果此时 enctype 属性为 multipart/form-data,则请求体则是添加了分隔符、参数描述信息等内容。

enctype 属性表

属性值说明
application/x-www-form-urlencoded数据被编码为名称/值对,这是默认的编码格式
multipart/form-data数据被编码为一条消息,每个控件对应消息中的一个部分
text/plain数据以纯文本形式进行编码,其中不含任何控件或格式字符

Gin 框架中下列方法可以用处理表单数据:

    // PostForm 从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,不存在则为空""
    func (c *Context) PostForm(key string) string

    // DefaultPostForm 从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,
    // 不存在则返回指定的值
    func (c *Context) DefaultPostForm(key, defaultValue string) string

    // GetPostForm 类似 PostForm(key).从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,
    // 如参数存在(即使值为"")则返回 (value, true),不存在的参数则返回指定的值 ("", false)。
    // 例如:
    //   email=mail@example.com  -->  ("mail@example.com", true) := GetPostForm("email")
     //  email 为 "mail@example.com"
    //   email=                  -->  ("", true) := GetPostForm("email") // email 值为 ""
    //                           -->  ("", false) := GetPostForm("email") // email 不存在
    func (c *Context) GetPostForm(key string) (string, bool)

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串切片,
    // 切片的长度与指定参数的值多少有关
    func (c *Context) PostFormArray(key string) []string

    //
    func (c *Context) getFormCache()

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串切片,
    // 至少一个值存在则布尔值为true
    func (c *Context) GetPostFormArray(key string) ([]string, bool)

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典
    func (c *Context) PostFormMap(key string) map[string]string

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典,
    // 至少一个值存在则布尔值为true
    func (c *Context) GetPostFormMap(key string) (map[string]string, bool)

    // 返回表单指定参数的第一个文件
    func (c *Context) FormFile(name string) (*multipart.FileHeader, error)

    // 分析multipart表单,包括文件上传
    func (c *Context) MultipartForm() (*multipart.Form, error)

    // 将表单文件上传到特定dst
    func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error

参数和表单混合处理

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

	router.POST("/post", func(c *gin.Context) {

		id := c.Query("id")
		page := c.DefaultQuery("page", "0")
		name := c.PostForm("name")
		message := c.PostForm("message")

		fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
	})
	router.Run(":8080")
}

下面是请求 Request 的头信息,分为四部分,请求行,请求头,空行,请求体:输入输出

POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "name=manu&message=this_is_great" "http://localhost:8080/post?id=1234&page=1"
id: 1234; page: 1; name: manu; message: this_is_great

参考:
https://github.com/gin-gonic/gin
https://github.com/gin-gonic/examples
https://gin-gonic.com/docs/introduction/

https://www.jianshu.com/p/a31e4ee25305
https://blog.csdn.net/u014361775/article/details/80582910
https://learnku.com/docs/gin-gonic/1.7/examples-ascii-json/11362

 类似资料: