Gin framework learning notes (02) - parameters are automatically bound to structures

Posted by TRemmie on Sat, 18 Dec 2021 03:45:24 +0100

The parameter binding model can automatically bind the request body to the structure body. At present, the request types supporting binding include JSON, XML, YAML and standard form data foo = Bar & boo = Baz. In other words, as long as the structure is defined, the data contained in the request can be automatically received, which is a very magical function of Gin framework.

When defining the structure corresponding to binding, you need to set the binding type label for the structure field. For example, when Binding JSON data, set the field label to json:"fieldname". Binding can be used to transfer data to data objects in the program more quickly.

When using a series of binding related methods in the Gin framework, Gin will infer how to bind according to the content type in the request header, that is, automatic binding. However, if the binding type is specified, developers can also use methods such as MustBindWith() or BindJSON() without automatic inference. You can specify that a field in the structure is required. The field needs to be labeled binding:"required". However, if it is a null value during binding, Gin will report an error.

In the binding package of the Gin framework, multiple MIME types of content type request header information are defined for type discrimination during automatic binding, and then the corresponding processing methods are adopted:

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

Among all bound methods, first, the c.Bind() method will infer a binding instance object according to the content type. Because it will call the function func Default(method, contentType string) Binding, which instantiates the specific binding object according to the method and content type of the HTTP request. The following types can be instantiated:

var (
    JSON          = jsonBinding{}
    XML           = xmlBinding{}
    Form          = formBinding{}
    Query         = queryBinding{}
    FormPost      = formPostBinding{}
    FormMultipart = formMultipartBinding{}
    ProtoBuf      = protobufBinding{}
    MsgPack       = msgpackBinding{}
    YAML          = yamlBinding{}
    Uri           = uriBinding{}
    Header        = headerBinding{}
)

In the binding package, that is, the binding directory, you can see that each instance structure defines a series of processing methods in a separate file. c. After the bind () method obtains the binding instance object, it will call the c.MustBindWith(obj, b) method. B is an instantiated binding object. For example, the c.BindJSON() method also calls c.MustBindWith(obj, b) because it knows that the instantiated object is JSON. Here, B is a jsonBinding {} object. Other processes such as XML are similar.

The c.MustBindWith() method will call the c.ShouldBindWith() method uniformly. In the c.ShouldBindWith() method, the processing method of the specific instance will be called: b.Bind(c.Request, obj). This b.Bind() method is very key. Each binding instance object implements this method, which realizes the binding function of parameters.

In the process of parameter binding, it can be roughly regarded as this process:

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

In parameter binding, whether c.Bind() or c.ShouldBindWith() methods are used, the parameters are bound to the structure pointer through the b.Bind() method of the specific instance. This instance can find the implementation file of its method in the binding directory. For example: JSON go , uri.go and form Go and other files, file names correspond to different content types.

In the Gin framework, the following methods can be used to handle binding:

// Bind checks the content type to automatically select the binding engine
// Rely on the "content type" header to use different bindings
//     "application/json" binding JSON
//     "application/xml" binding XML
// Otherwise, an error message is returned
// If content type = = "application / json", JSON or XML is input as JSON,
// Bind resolves the body of the request to JSON.
// It decodes the JSON payload into a structure specified as a pointer.
// If the input is invalid, it writes a 400 error and sets the content type header "text / plain" in the response.
func (c *Context) Bind(obj interface{}) error

// BindJSON is short for c.MustBindWith(obj, binding.JSON)
func (c *Context) BindJSON(obj interface{}) error

// BindXML is short for c.MustBindWith(obj, binding.BindXML)
func (c *Context) BindXML(obj interface{}) error

// BindQuery is short for c.MustBindWith(obj, binding.Query)
func (c *Context) BindQuery(obj interface{}) error

// BindYAML is short for c.MustBindWith(obj, binding.YAML)
func (c *Context) BindYAML(obj interface{}) error

// BindHeader is short for c.MustBindWith(obj, binding.Header)
func (c *Context) BindHeader(obj interface{}) error

// BindUri uses binding Structure pointer passed by URI binding.
// If any error occurs, it will abort the request using HTTP 400.
func (c *Context) BindUri(obj interface{}) error

// MustBindWith uses the specified binding engine to bind the passed struct pointer.
// If any error occurs, it will abort the request using HTTP 400.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error

// ShouldBind checks the content type to automatically select the binding engine
// Rely on the "content type" header to use different bindings
//     "application/json" binding JSON
//     "application/xml" binding XML
// Otherwise, an error message is returned
// If content type = = "application/json", JSON or XML is used as JSON input,
// Bind resolves the body of the request to JSON.
// It decodes the JSON payload into a structure specified as a pointer.
// Similar to c.Bind(), but this method does not support writing 400 to the response when JSON is invalid.
func (c *Context) ShouldBind(obj interface{}) error

// ShouldBindJSON is short for c.ShouldBindWith(obj, binding.JSON)
func (c *Context) ShouldBindJSON(obj interface{}) error

// ShouldBindXML is short for c.ShouldBindWith(obj, binding.XML)
func (c *Context) ShouldBindXML(obj interface{}) error

// ShouldBindQuery is short for c.ShouldBindWith(obj, binding.Query)
func (c *Context) ShouldBindQuery(obj interface{}) error

// ShouldBindYAML is short for c.ShouldBindWith(obj, binding.YAML)
func (c *Context) ShouldBindYAML(obj interface{}) error

// ShouldBindHeader is short for c.ShouldBindWith(obj, binding.Header)
func (c *Context) ShouldBindHeader(obj interface{}) error

// ShouldBindUri uses the specified binding engine to bind the passed struct pointer.
func (c *Context) ShouldBindUri(obj interface{}) error

// ShouldBindWith uses a custom binding engine to bind the struct pointer passed.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error

// ShouldBindBodyWith is similar to ShouldBindWith, but it stores requests
// ShouldBindBodyWith can enter the context and be reused when called again.
//
// Note: this method reads the body before binding. So it is recommended
// ShouldBindWith can achieve better performance if you only need to call it once.
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error)

1. Bind query string or form data

Form and URLQuery pass parameters, and the program obtains parameter values by binding, which makes the extraction of parameters more automatic.

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
    // If it is a 'GET' request, only the 'Form' binding engine (` query ') is used.
    // If it is a 'POST' request, first check whether the 'content type' is' JSON 'or' XML ',
    // Then use 'Form' (` Form data ').
    if c.ShouldBind(&person) == nil {
        log.Println(person.Name)
        log.Println(person.Address)
    }

    c.String(200, "Success")
}

When the program is running in Debug mode, run the following three commands on the command line:

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

Output results:

[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

Through the POST method, it can be bound and parsed normally by using Urlencoded coding or JSON. However, if the program receiving method is changed to GET method:

route.GET("/testing", startPage)

Parameters can only be passed through URL Query:

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

In this way, parameters passed through URL Query can also be bound normally.

2. Multipart/Urlencoded binding

Pass the parameter through the form, and the following program obtains the parameter value by binding.

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
        // Forms can be explicitly bound
        // c.ShouldBindWith(&form, binding.Form)

        // Or simply use the ShouldBind method to automatically bind
        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")
}

The label of the structure in the above program: form:"user", indicating that the name in the form is user.

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

When the program is running in Debug mode, run the following two commands on the command line:

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 parameter binding

The Gin framework supports the existence of parameters in the routing uri and the binding of these parameters. You need to specify the field label as uri in the structure.

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

The label of the structure in the above program: uri:"id", indicating that the parameter name in the URI is id.

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

When the program is running in Debug mode, run the following command on the command line:

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

4. Bind HTML check box

The Gin framework can easily get the value of HTML FORM element by binding. You need to specify the field label form:filedname in the structure.

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

        // Simply use the ShouldBind method to automatically bind
        if c.ShouldBind(&form) == nil {
            c.JSON(200, gin.H{"color": form.Colors})
        }
    })
    router.Run(":8080")
}

index. The HTML file is placed in the public directory under the program directory.

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

Note that the structure label: colors [] in the above program is consistent with the name of the check box, which represents an array, so you can get the values of multiple selected items.
Run the program and access it through the browser http://localhost:8080/ , a check box form appears, select more than two options, here select red and green, and then submit the form (the request is sent to http://localhost:8080/check ).

The page displays the options that match the submission:

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

5. Bind form data to embedded structure

It has been known that data can be automatically obtained to simple structure objects through binding. For embedded structures, data can also be obtained automatically through binding, but do not specify labels behind embedded structures.

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

type StructB struct {
    NestedStruct StructA // Do not specify a label
    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()
}

Input and output results:

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. Bind the request body to different structures

Generally, data is bound by calling the ShouldBind() method, but note that this method cannot be called multiple times in some cases.

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 uses c.request Body, not reusable.
    if errA := c.ShouldBind(&objA); errA != nil {
        fmt.Println(errA)
        c.String(http.StatusOK, `the body should be formA`)
        // Because now c.request The body is EOF, so an error will be reported here.
    } 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")
}

Run the program and access it through the browser http://localhost:8080/bind?foo=foo&bar=bar , the page displays:
the body should be formA
When the program is running in Debug mode, run the following command on the command line:

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

Command return:
the body should be formB
Indicates that an error occurred during the second run of the ShouldBind() method. To bind multiple times, you can use the c.ShouldBindBodyWith() method.

func BindHandler(c *gin.Context) {
    objA := formA{}
    objB := formB{}
    // ShouldBindBodyWith() read c.request Body and store the results in the context.
    if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA != nil {

        fmt.Println(errA)
        c.String(http.StatusOK, `the body should be formA`)
        // At this time, the body stored in the context is reused.
    } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB != nil {
        fmt.Println(errB)
        c.String(http.StatusOK, `the body should be formB JSON`)
        // Other formats are acceptable
    } else {
        c.String(http.StatusOK, `Success`)
    }
}

c.ShouldBindBodyWith() stores the request body in the context before binding. This will have a slight impact on performance. If the binding can be completed in one call, do not use this method.
Only some formats require this function, such as JSON, XML, MsgPack, ProtoBuf. For other formats, such as Query, form, FormPost and FormMultipart, c.ShouldBind() can be called multiple times without causing any performance loss. This is also the reason why the label in the previous structure does not define a form, but only defines json:"foo" xml:"foo" binding:"required".

7. Bind only URL Query parameters

ShouldBind() method supports URL Query parameter binding and POST parameter binding. The ShouldBindQuery() method only binds URL Query parameters and ignores POST data.

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

Run the program and access it through the browser http://localhost:8080/ , the page displays "Sucess". Output results:

[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

The output shows that the URL Query parameter can be bound normally by the program through the GET method. Note that the Any() method is used in the above program, which can match many HTTP methods.
If the program continues to run in Debug mode, run the following command on the command line:

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

The return of the command line indicates that the request has been successfully submitted through the POST method, and the server returns successfully. The status code is 200 and the return content is Success.
Console output results:

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

From the console output, you can see that the data submitted through POST is not bound normally. However, you can bind normally through the ShouldBind() method. This indicates that ShouldBindQuery() only binds URL Query parameters and ignores POST data.

8. JSON model binding

Submit JSON format data through POST method. The program obtains JSON data by binding and passes it to the structure, but the field label needs to be specified as JSON.

// Bind 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()

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

    // Bind HTML form (user = manu & password = 123)
    router.POST("/loginForm", func(c *gin.Context) {
        var form Login
        // Infer which binder to use from 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"})
    })

    // Listen and start the service
    router.Run(":8080")
}

Output results:

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 information binding

The header can also pass parameters. The program obtains the parameter value by binding. It needs to be specified as the header on the field label of the structure.

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

Run command

curl -H "rate:300" -H "domain:music" http://localhost:8080/

{"Domain":"music","Rate":300}

The curl command carries the customized header information to the Handler handler, and the ShouldBindHeader() method automatically binds the header variable to the structure.

reference resources: https://gitbook.cn/gitchat/column/5dab061e7d66831b22aa0b44/topic/5dab09f37d66831b22aa0b5d