侧边栏壁纸
  • 累计撰写 16 篇文章
  • 累计创建 17 个标签
  • 累计收到 1 条评论

Gin框架ShouldBind/ShouldBindBodyWith源码分析

xiuxiubiu
2019-08-20 / 0 评论 / 0 点赞 / 2,011 阅读 / 6,496 字 / 正在检测是否收录...

read读操作会移动指针,比如两次调用 ioutil.ReadAll(c.Request.Body),只有第一次能读出数据。

今天看Gin官方文档示例 将request body绑定到不同的结构体中

第一个示例使用c.ShouldBind绑定数据

// 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`)
}

第二个示例使用c.ShouldBindBodyWith多次绑定数据

// 读取 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`)
}

那么这两个方法有什么区别呢?

文档有两段描述:

c.ShouldBindBodyWith 会在绑定之前将 body 存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。

c.ShouldBindBodyWith是如何将body存储到上下文中的?源码如下:

// 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.
func (c *Context) ShouldBindBodyWith(
	obj interface{}, bb binding.BindingBody,
) (err error) {
	var body []byte
	if cb, ok := c.Get(BodyBytesKey); ok {
		if cbb, ok := cb.([]byte); ok {
			body = cbb
		}
	}
	if body == nil {
		body, err = ioutil.ReadAll(c.Request.Body)
		if err != nil {
			return err
		}
		c.Set(BodyBytesKey, body)
	}
	return bb.BindBody(body, obj)
}

首先,调用c.Get(BodyBytesKey)从上下文获取body数据,
如果body为nil,调用ioutil.ReadAll(c.Request.Body)读取body数据,
并将body通过c.Set(BodyBytesKey, body)存储到上下文中。
然后调用bb.BindBody将body绑定到结构体

// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
	Binding
	BindBody([]byte, interface{}) error
}

通过源码我们发现只有JSON, XML, MsgPack, ProtoBuf格式实现了BindingBody接口

比如jsonBinding的源码如下:

// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package binding

import (
	"bytes"
	"fmt"
	"io"
	"net/http"

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

// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false

type jsonBinding struct{}

func (jsonBinding) Name() string {
	return "json"
}

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
	if req == nil || req.Body == nil {
		return fmt.Errorf("invalid request")
	}
	return decodeJSON(req.Body, obj)
}

func (jsonBinding) BindBody(body []byte, obj interface{}) error {
	return decodeJSON(bytes.NewReader(body), obj)
}

func decodeJSON(r io.Reader, obj interface{}) error {
	decoder := json.NewDecoder(r)
	if EnableDecoderUseNumber {
		decoder.UseNumber()
	}
	if err := decoder.Decode(obj); err != nil {
		return err
	}
	return validate(obj)
}

接下来会分析为什么只有JSON等这几个格式实现BindingBody接口。⬇️

只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 对于其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失。

为什么只有JSON, XML需要此功能?Query, Form等等格式不需要功能?ShouldBindShouldBindJson等源码如下:

// ShouldBind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
//     "application/json" --> JSON binding
//     "application/xml"  --> XML binding
// otherwise --> returns an error
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer.
// Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid.
func (c *Context) ShouldBind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.ShouldBindWith(obj, b)
}

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

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

// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}

我们发现,这几个方法都是通过ShouldBindWith,然后调用bingding.Binding类型的Bind方法绑定数据。

bingding.Binding接口源码如下:

// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}

通过gin-gonic/gin/binding/form.go可知formBindingformPostBindingformMultipartBinding会调用http.Request.ParseForm()获取form数据,例如formBindingBind方法实现如下:

func (formBinding) Bind(req *http.Request, obj interface{}) error {
	if err := req.ParseForm(); err != nil {
		return err
	}
	if err := req.ParseMultipartForm(defaultMemory); err != nil {
		if err != http.ErrNotMultipart {
			return err
		}
	}
	if err := mapForm(obj, req.Form); err != nil {
		return err
	}
	return validate(obj)
}

ParseForm方法会将body数据保存到http.Request.Form中:

func (r *Request) ParseForm() error {
	var err error
	if r.PostForm == nil {
		if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
			r.PostForm, err = parsePostForm(r)
		}
		if r.PostForm == nil {
			r.PostForm = make(url.Values)
		}
	}
	if r.Form == nil {
		if len(r.PostForm) > 0 {
			r.Form = make(url.Values)
			copyValues(r.Form, r.PostForm)
		}
		var newValues url.Values
		if r.URL != nil {
			var e error
			newValues, e = url.ParseQuery(r.URL.RawQuery)
			if err == nil {
				err = e
			}
		}
		if newValues == nil {
			newValues = make(url.Values)
		}
		if r.Form == nil {
			r.Form = newValues
		} else {
			copyValues(r.Form, newValues)
		}
	}
	return err
}

queryBinding是通过调用http.Requst.URL.Query()通过url地址解析body,不涉及io.Reader,所以也就不存在读取完无法再次获取的问题,也就不需要保存到上下文实现多次绑定的问题:

func (queryBinding) Bind(req *http.Request, obj interface{}) error {
	values := req.URL.Query()
	if err := mapForm(obj, values); err != nil {
		return err
	}
	return validate(obj)
}

所以Query, Form, FormPost, FormMultipart并不需要将body再次保存到上下文中实现多次绑定,也就不用实现上边说到的BindingBody接口。

jsonBinding, xmlBinding, msgpackBinding, protobufBinding都是直接从http.Request读取body,并没有保存读取的数据,所以调用c.ShouldBind()多次绑定会报错,因为只有第一次可以读取出数据。

例如jsonBindingBind方法源码:

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
	if req == nil || req.Body == nil {
		return fmt.Errorf("invalid request")
	}
	return decodeJSON(req.Body, obj)
}

func (jsonBinding) BindBody(body []byte, obj interface{}) error {
	return decodeJSON(bytes.NewReader(body), obj)
}

func decodeJSON(r io.Reader, obj interface{}) error {
	decoder := json.NewDecoder(r)
	if EnableDecoderUseNumber {
		decoder.UseNumber()
	}
	if err := decoder.Decode(obj); err != nil {
		return err
	}
	return validate(obj)
}
0

评论