Gin框架 -- 请求

0. RestfulAPI

即Representational State Transfer的缩写。直接翻译的意思是"表现层状态转化",是一种互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作

1.获取文章 /blog/getXxx Get blog/Xxx

2.添加 /blog/addXxx POST blog/Xxx

3.修改 /blog/updateXxx PUT blog/Xxx

4.删除 /blog/delXxxx DELETE blog/Xxx

1. URL参数

0. 示例请求

http://127.0.0.1:8888/user/info/1/张三

1. Params.ByName()

1. 取值方法

context.Params.ByName()

2. 示例

package main

import (
    "net/http"
    "strings"

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

func main() {
	r := gin.Default()
	r.GET("/user/:id/*name", func(context *gin.Context) {
         //:name 只会接收值,*name会接收完URL路径:/xxx 
		userId := context.Params.ByName("id")
         // userName的值为 /张三 
		userName := context.Params.ByName("name")
         // strings.Trim会将"/" 从字符串中切掉,得到的仍旧是string
         // 类似python 中的split函数
		action := strings.Trim(userName, "/")
		context.String(http.StatusOK, name +" is "+action)
	})
	r.Run(":8002")
}

3. 测试
image-20230602164408818

2. Param()

package main

import (
    "net/http"
    "strings"

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

func main() {
	r := gin.Default()
	r.GET("/user/:id/*name", func(context *gin.Context) {
         //:name 只会接收值,*name会接收完URL路径:/xxx 
		userId := context.Param("id")
         // userName的值为 /张三 
		userName := context.Param("name")
         // strings.Trim会将"/" 从字符串中切掉,得到的仍旧是string
         // 类似python 中的split函数
		action := strings.Trim(userName, "/")
		context.String(http.StatusOK, name +" is "+action)
	})
	r.Run(":8002")
}

2. 关键字参数

0. 示例请求

http://127.0.0.1:8002/user?id=1&name=xiaoming

1. DefaultQuery()

1. 取值方法

context.DefaultQuery()

2. 示例

name := c.DefaultQuery("name","如果没取到则使用本默认值")    
fmt.Println(name)

2. Query()

1. 取值方法

context.Query()

2. 示例

name := c.Query("name")
fmt.Println(name)

//如果没有传name参数, 则为空字符串,因为name是字符串类型,当没有获取到任何值时,name为定义时的空字符串

3. 表单参数

表单传输为post请求,http常见的传输格式为四种:

  1. application/json
  2. application/x-www-form-urlencoded
  3. application/xml
  4. multipart/form-data

表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数

1. PostForm()

1. 取值方法

context.PostForm()

2. 示例

package main

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

func main() {
	r := gin.Default()
	r.POST("/login", func(context *gin.Context) {
         // 获取值,如果没有获取到则为空字符串
		username := context.PostForm("username")
		password := context.PostForm("password")
		fmt.Println(username,password)
		context.String(http.StatusOK, "登录成功")
	})
	r.Run(":8002")
}

2. GetPostForm()

package main

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

func main() {
	r := gin.Default()
	r.POST("/login", func(context *gin.Context) {
         // 返回值为值,和bool
		username,isgeted := context.GetPostForm("username")
		if !isgeted{
			context.String(http.StatusBadRequest, "用户名错误")
		}
		password,isgeted := context.GetPostForm("password")
		if !isgeted{
			context.String(http.StatusBadRequest, "密码错误")
		}
		fmt.Println(username,password)
		context.String(http.StatusOK, "登录成功")
	})
	r.Run(":8002")
}

3. DefaultPostForm()

4. 数据绑定

1. 概述

参数绑定模型可以将请求体自动绑定到结构体中,目前支持绑定的请求类型有 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)

2. Json参数

0. 结构体

package data

type CookieStr struct {
	Cookiestr string `json:"cookiestr"`
}

type Login struct {
	UserName string `json:"user_name" binding:"required"`
	PassWord string `json:"pass_word" binding:"required"`
}

1. ShouldBind()

ShouldBind()会根据content-type推断使用哪个数据绑定函数,如果是json数据必须content-type="application/json"

package auth

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

func loginHandler(ctx *gin.Context) {
    
	var loginJson data.Login
    // 你可以使用显式绑定声明绑定 multipart form:
	// c.ShouldBindWith(&loginJson, binding.Form)
	// 或者简单地使用 ShouldBind 方法自动绑定:
    // 在这种情况下,将自动选择合适的绑定
    // 如果是 `GET` 请求,只使用 `Form` 绑定引擎(`query`)。
    // 如果是 `POST` 请求,首先检查 `content-type` 是否为 `JSON` 或 `XML`,
    // 然后再使用 `Form`(`form-data`)。
	if err := ctx.ShouldBind(&loginJson); err != nil {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"数据错误",
		})
        return
	}
	// 判断用户名密码是否正确
	if loginJson.UserName != "xiaoming" && loginJson.PassWord != "123" {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"账号或密码错误",
		})
        return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"sucess_message":"Json 登录成功",
		"data":&loginJson,
	})
    return
}

请求与响应

注意必须与结构体定义的tag名,一致

2. ShouldBindJson()

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

3. 表单参数(请求体)

0. 结构体

type Login struct {
	UserName string `form:"userName" binding:"required"`
	PassWord string `form:"passWord" binding:"required"`
}

1. Bind()

结构体构建

package data

type CookieStr struct {
	Cookiestr string `json:"cookiestr"`
}

type Login struct {
	UserName string `form:"userName" binding:"required"`
	PassWord string `form:"passWord" binding:"required"`
}

后端数据解析

package auth

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

func loginHandler(ctx *gin.Context) {
	var loginForm data.Login
	//var cookieJson data.CookieStr
	if err := ctx.Bind(&loginForm); err != nil {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"数据错误",
		})
        return
	}
	// 判断用户名密码是否正确
	if loginForm.UserName != "xiaoming" && loginForm.PassWord != "123" {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"账号或密码错误",
		})
        return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"sucess_message":"Form 登录成功",
		"data":&loginForm,
	})
    return
}

请求与响应

注意必须与结构体上的tag名,一致

2. ShouldBind()

// 同上逻辑一样

// 可显式绑定表单
// c.ShouldBindWith(&form, binding.Form)

// 或者简单地使用 ShouldBind 方法自动绑定
if c.ShouldBind(&form) == nil {}

4. URL关键字参数

0. 结构体

type UrlParams struct {
	Id string `form:"id"`
	Page string `form:"page"`
}

1. ShouldBind()

func UrlParamsHandler(ctx *gin.Context) {
	var urlParams data.UrlParams
    // 
	err := ctx.ShouldBind(&urlParams)
    // 只绑定?后面的查询字符串
    // if c.ShouldBindQuery(&person) == nil {}
	if err != nil{
		fmt.Println(err.Error())
	}
	fmt.Println(urlParams)
}

5. URL位置参数

0. 结构体

package data

type CookieStr struct {
	Cookiestr string `json:"cookiestr"`
}

type Login struct {
	UserName string ` uri:"uname" binding:"required"`
	PassWord string ` uri:"pwd" binding:"required"`
}

1. ShouldBindUri()

routers.go

e.GET("/home/:uname/:pwd",HomeHandler)

handler.go

package auth

import (
	"ginproject/data"
	"github.com/gin-gonic/gin"
	"net/http"
)
func HomeHandler(ctx *gin.Context)  {
	var loginUrl data.Login
	//var cookieJson data.CookieStr
	if err := ctx.ShouldBindUri(&loginUrl); err != nil {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"数据错误",
		})
        return
	}
	// 判断用户名密码是否正确
	if loginUrl.UserName != "xiaoming" && loginUrl.PassWord != "123" {
		ctx.JSON(http.StatusOK, gin.H{
			"err_message":"账号或密码错误",
		})
        return
	}
	ctx.JSON(http.StatusOK, gin.H{
		"sucess_message":"Url 登录成功",
		"data":&loginUrl,
	})
    return
}

请求与响应,注意必须与结构体上的tag名,一致

6. HTML复选框参数

0. HTML

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

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>

1. 结构体

type CheckForm struct {
    Colors []string `form:"colors[]"`   // 与html中的name值相同
}

注意,上面程序中结构体标签: colors[] 与复选框的名字一致,这里表示数组所以可以得到多个已选项的值。
运行程序,通过浏览器访问 http://localhost:8080/ ,出现复选框表单,选择两个以上选项,这里选择红,绿两种颜色,然后提交表单(请求发送到 http://localhost:8080/check )。

2. ShouldBind()

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

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

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

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

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

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

8. 将请求体绑定到不同的结构体中

一般通过调用 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

命令返回:
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" 的原因。

9. 只绑定 URL Query 参数

houldBind() 方法支持 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

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

10. 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() 方法自动绑定头部变量到结构体。

11. 请求体参数绑定到不同的结构体

一般通过调用 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{}
  // c.ShouldBind 使用了 c.Request.Body,不可重用。
  if errA := c.ShouldBind(&objA); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // 因为现在 c.Request.Body 是 EOF,所以这里会报错。
  } 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{}
  // 读取 c.Request.Body 并将结果存入上下文。
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // 这时, 复用存储在上下文中的 body。
  } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // 可以接受其他格式
  } 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。 对于其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失 (详见 https://github.com/gin-gonic/gin/pull/1341)。

12. 多种形式的参数

一个post请求,既有url参数,又有请求体参数,如何绑定到结构体

// http://127.0.0.1:8009/url_params2/1/5/

路由

e.POST("/url_params2/:id/:page",UrlParams2Handler)

创建结构体

type UrlParams struct {
	Id string `uri:"id" binding:"required"`
	Page string `uri:"page" binding:"required"`
	Name   string `form:"name" binding:"required"`
	Message string `form:"message" binding:"required"`
}

取值

func UrlParams2Handler(ctx *gin.Context)  {
	var urlParamsQuery data.UrlParams
	var urlParamsParams data.UrlParams
	err := ctx.ShouldBindUri(&urlParamsQuery)
	err = ctx.Bind(&urlParamsParams)
	if err != nil{
		fmt.Println(err.Error())
	}
	fmt.Println(urlParamsQuery,urlParamsParams)
}

5. 校验器

1. 内置校验器

1. 结构体tag校验

用gin框架的数据验证,可以不用解析数据,减少if else,会简洁许多。

package main

import (
    "fmt"
    "time"

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

//Person ..
type Person struct {
    //不能为空并且大于10
    Age      int       `form:"age" binding:"required,gt=10"`
    Name     string    `form:"name" binding:"required"`
    Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func main() {
    r := gin.Default()
    r.GET("/5lmh", func(c *gin.Context) {
        var person Person
        if err := c.ShouldBind(&person); err != nil {
            c.String(500, fmt.Sprint(err))
            return
        }
        c.String(200, fmt.Sprintf("%#v", person))
    })
    r.Run()
}

2. 常用校验参数

多个条件以 , 分割


required              // 必填
email 			     // 应用email来限度字段必须是邮件模式,间接写eamil即可,无需加任何指定。
omitempty 		     // 字段未设置,则疏忽
-                     // 跳过该字段,不测验;
|				    // 应用多个束缚,只须要满足其中一个,例如rgb|rgba;
unique 			    // 指定唯一性束缚,不同类型解决不同:

				   // 1. 对于map,unique束缚没有反复的值
				   // 2. 对于数组和切片,unique没有反复的值
				   // 3. 对于元素类型为构造体的碎片,unique束缚构造体对象的某个字段不反复,应用unique=field指定字段名

ne=				   // 不等于参数值,例如ne=5;
gt=                 // 大于
gte=                // 大于等于
lt=                 // 小于
lte=                // 小于等于
eq=                 // 等于参数值,留神与len不同。对于字符串,eq束缚字符串自身的值,而len束缚字符串长度。例如eq=10;
len=			   // 等于参数值,例如len=10;
max=			   // 小于等于参数值,例如max=10;
min=			   // 大于等于参数值,例如min=10
oneof=              // 只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号突围,例如 oneof=male female。
excludesall=        // 不蕴含参数中任意的 UNICODE 字符,例如excludesall=ab,不能包含ab
excludesrune=       // 不蕴含参数示意的 rune 字符,excludesrune=asong;
startswith=         // 以参数子串为前缀,例如startswith=hi;
endswith=		   // 以参数子串为后缀,例如endswith=bye。
contains=		   // 蕴含参数子串,例如contains=email;
containsany=       // 蕴含参数中任意的 UNICODE 字符,例如containsany=ab;
containsrune=	   // 蕴含参数示意的 rune 字符,例如`containsrune=asong;
excludes=          // 不蕴含参数子串,例如excludes=email;
eqfield=		  // 定义字段间的相等束缚,用于束缚同一构造体中的字段。例如:eqfield=Password
eqcsfield=		  // 束缚对立构造体中字段等于另一个字段(绝对),确认明码时能够应用,例如:eqfiel=ConfirmPassword
nefield=		  // 用来束缚两个字段是否雷同,确认两种色彩是否统一时能够应用,例如:nefield=Color1
necsfield=		  // 束缚两个字段是否雷同(绝对)


msg:""            // 当字段检验未通过时, 返回前端的错误信息

2. 第三方校验器校验

1. 包下载

github.com/go-playground/validator/v10

2. 语法格式

type Test struct {
	Field `validate:"min=10,max=0"`
}

3. 常用校验参数

required              // 必填
email 			     // 应用email来限度字段必须是邮件模式,间接写eamil即可,无需加任何指定。
omitempty 		     // 字段未设置,则疏忽
-                     // 跳过该字段,不测验;
|				    // 应用多个束缚,只须要满足其中一个,例如rgb|rgba;
unique 			    // 指定唯一性束缚,不同类型解决不同:

				   // 1. 对于map,unique束缚没有反复的值
				   // 2. 对于数组和切片,unique没有反复的值
				   // 3. 对于元素类型为构造体的碎片,unique束缚构造体对象的某个字段不反复,应用unique=field指定字段名

ne=""				// 不等于参数值,例如ne=5;
gt=""                // 大于
gte=""               // 大于等于
lt=""                // 小于
lte=""               // 小于等于
eq=""                // 等于参数值,留神与len不同。对于字符串,eq束缚字符串自身的值,而len束缚字符串长度。例如eq=10;
len=""				// 等于参数值,例如len=10;
max=""				// 小于等于参数值,例如max=10;
min=""				// 大于等于参数值,例如min=10
oneof= ""             // 请求参数必须只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号突围,例如 oneof=male female。
excludesall=""        // 不蕴含参数中任意的 UNICODE 字符,例如excludesall=ab,不能包含ab
excludesrune=""       // 不蕴含参数示意的 rune 字符,excludesrune=asong;
startswith=""         // 以参数子串为前缀,例如startswith=hi;
endswith=""		     // 以参数子串为后缀,例如endswith=bye。
contains=""			 // 蕴含参数子串,例如contains=email;
containsany=""        // 蕴含参数中任意的 UNICODE 字符,例如containsany=ab;
containsrune=""		 // 蕴含参数示意的 rune 字符,例如`containsrune=asong;
excludes=""          // 不蕴含参数子串,例如excludes=email;
eqfield=""			// 定义字段间的相等束缚,用于束缚同一构造体中的字段。例如:eqfield=Password
eqcsfield=""		// 束缚对立构造体中字段等于另一个字段(绝对),确认明码时能够应用,例如:eqfiel=ConfirmPassword
nefield=""			// 用来束缚两个字段是否雷同,确认两种色彩是否统一时能够应用,例如:nefield=Color1
necsfield=""		// 束缚两个字段是否雷同(绝对)

3. 自定义校验器

1. 官网

https://pkg.go.dev/gopkg.in/go-playground/validator.v10

2. 示例

package main

import (
    "net/http"
    "reflect"
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "gopkg.in/go-playground/validator.v8"
)

/*
    对绑定解析到结构体上的参数,自定义验证功能
    比如我们要对 name 字段做校验,要不能为空,并且不等于 admin ,类似这种需求,就无法 binding 现成的方法
    需要我们自己验证方法才能实现 官网示例(https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Custom_Functions)
    这里需要下载引入下 gopkg.in/go-playground/validator.v8
*/
type Person struct {
    Age int `form:"age" binding:"required,gt=10"`
    // 2、在参数 binding 上使用自定义的校验方法函数注册时候的名称
    Name    string `form:"name" binding:"NotNullAndAdmin"`
    Address string `form:"address" binding:"required"`
}

// 1、自定义的校验方法
func nameNotNullAndAdmin(v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string) bool {

	if value, ok := field.Interface().(string); ok {
		// 字段不能为空,并且不等于  admin
		return value != "" && !("5lmh" == value)
	}

	return true
}

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

    // 3、将我们自定义的校验方法注册到 validator中
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // 这里的 key 和 fn 可以不一样最终在 struct 使用的是 key
        v.RegisterValidation("NotNullAndAdmin", nameNotNullAndAdmin)
    }

    /*
        curl -X GET "http://127.0.0.1:8080/testing?name=&age=12&address=beijing"
        curl -X GET "http://127.0.0.1:8080/testing?name=lmh&age=12&address=beijing"
        curl -X GET "http://127.0.0.1:8080/testing?name=adz&age=12&address=beijing"
    */
    r.GET("/5lmh", func(c *gin.Context) {
        var person Person
        if e := c.ShouldBind(&person); e == nil {
            c.String(http.StatusOK, "%v", person)
        } else {
            c.String(http.StatusOK, "person bind err:%v", e.Error())
        }
    })
    r.Run()
}

3. 示例

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
	"net/http"
	"time"
)

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
	date, ok := fl.Field().Interface().(time.Time)
	if ok {
		today := time.Now()
		if today.After(date) {
			return false
		}
	}
	return true
}

type Booking struct {
	// binding中绑定校验器bookableDate,time_format:"2006-01-02"为goalng的时间格式化规则
	CheckIn  time.Time `json:"check_in" form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
	CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}

func main() {
	route := gin.Default()
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		// 注册校验器
		v.RegisterValidation("bookabledate", bookableDate)
	}

	route.GET("/bookable", func(context *gin.Context) {
		var b Booking
		// 将数据进行数据绑定,如果发生错误,则表示没有通过校验
		if err := context.ShouldBindWith(&b, binding.Query); err == nil {
			context.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
		} else {
			context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	route.Run(":8999")
}

4. 多语言翻译检验信息响应

1. 官网

https://pkg.go.dev/gopkg.in/go-playground/validator.v10

2. 示例

当业务系统对验证信息有特殊需求时,例如:返回信息需要自定义,手机端返回的信息需要是中文而pc端发挥返回的信息需要时英文,如何做到请求一个接口满足上述三种情况。

package main

import (
    "fmt"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/locales/en"
    "github.com/go-playground/locales/zh"
    "github.com/go-playground/locales/zh_Hant_TW"
    ut "github.com/go-playground/universal-translator"
    "gopkg.in/go-playground/validator.v9"
    en_translations "gopkg.in/go-playground/validator.v9/translations/en"
    zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
    zh_tw_translations "gopkg.in/go-playground/validator.v9/translations/zh_tw"
)

var (
    Uni      *ut.UniversalTranslator
    Validate *validator.Validate
)

type User struct {
    Username string `form:"user_name" validate:"required"`
    Tagline  string `form:"tag_line" validate:"required,lt=10"`
    Tagline2 string `form:"tag_line2" validate:"required,gt=1"`
}

func main() {
    en := en.New()
    zh := zh.New()
    zh_tw := zh_Hant_TW.New()
    Uni = ut.New(en, zh, zh_tw)
    Validate = validator.New()

    route := gin.Default()
    route.GET("/5lmh", startPage)
    route.POST("/5lmh", startPage)
    route.Run(":8080")
}

func startPage(c *gin.Context) {
    //这部分应放到中间件中
    locale := c.DefaultQuery("locale", "zh")
    trans, _ := Uni.GetTranslator(locale)
    switch locale {
    case "zh":
        zh_translations.RegisterDefaultTranslations(Validate, trans)
        break
    case "en":
        en_translations.RegisterDefaultTranslations(Validate, trans)
        break
    case "zh_tw":
        zh_tw_translations.RegisterDefaultTranslations(Validate, trans)
        break
    default:
        zh_translations.RegisterDefaultTranslations(Validate, trans)
        break
    }

    //自定义错误内容
    Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
        return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
    }, func(ut ut.Translator, fe validator.FieldError) string {
        t, _ := ut.T("required", fe.Field())
        return t
    })

    //这块应该放到公共验证方法中
    user := User{}
    c.ShouldBind(&user)
    fmt.Println(user)
    err := Validate.Struct(user)
    if err != nil {
        errs := err.(validator.ValidationErrors)
        sliceErrs := []string{}
        for _, e := range errs {
            sliceErrs = append(sliceErrs, e.Translate(trans))
        }
        c.String(200, fmt.Sprintf("%#v", sliceErrs))
    }
    c.String(200, fmt.Sprintf("%#v", "user"))
}

正确的连接:http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=33&locale=zh

返回英文的验证信息: http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=en

返回中文的验证信息: http://localhost:8080/testing?user_name=枯藤&tag_line=9&tag_line2=3&locale=zh
查看更多的功能可以查看官网

6. 常用方法

1. 原生请求数据

context.GetRawData()
posted @ 2021-12-21 16:38  河图s  阅读(430)  评论(0)    收藏  举报