gin源码阅读(2)请求体中的JSON参数是如何解析的?

gin源码阅读(2)请求体中的JSON参数是如何解析的?

这篇文章中我们会来关注一下gin是如何解析请求体中的json参数的。

绑定请求体数据的四种方式

Gin 提供了四种可直接将请求体中的 JSON 数据解析并绑定至相应类型的函数,分别是:BindJSON, Bind, ShouldBindJSON, ShouldBind。下面讲解的不会太涉及具体的 JSON 解析算法,而是更偏向于 Gin 内部的实现逻辑。

其中,可为它们分为两类,一类为 Must Bind,另一类为 Should Bind,前缀为 Should 的皆属于 Should Bind,而以 Bind 为前缀的,则属于 Must Bind。正如其名,Must Bind 一类在对请求进行解析时,若出现错误,会通过 c.AbortWithError(400, err).SetType(ErrorTypeBind) 终止请求,这会把响应码设置为 400,Content-Type 设置为 text/plain; charset=utf-8,在此之后,若尝试重新设置响应码,则会出现警告,如将响应码设置为 200:[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 200;而 Should Bind 一类在对请求进行解析时,若出现错误,只会将错误返回,而不会主动进行响应,所以,在使过程中,如果对产生解析错误的行为有更好的控制,最好使用 Should Bind 一类,自行对错误行为进行处理。

我们先来看一下开始提到的 BindBindJSON 这两个函数的实现源代码:

go 复制代码
// gin v1.10.0 context.go

// Bind checks the Method and Content-Type to select a binding engine automatically,
// Depending on the "Content-Type" header different bindings are used, for example:
//
//	"application/json" --> JSON binding
//	"application/xml"  --> XML binding
//
// 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.
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj any) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}

// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj any) error {
	return c.MustBindWith(obj, binding.JSON)
}

// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
	if err := c.ShouldBindWith(obj, b); err != nil {
		c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
		return err
	}
	return nil
}

从上面的源码可以看到BindBindJSON其实调用的都是MustBindWith,而MustBindWith内部调用的又是ShouldBindWith函数。

常用的绑定JSON请求数据的ShouldBindJSON方法

从下面的源码中可以看到,常用的ShouldBindJSON(...)方法也是调用的ShouldBindWith方法。

go 复制代码
// gin v1.10.0 context.go

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

接下来我们就详细看看ShouldBindWith方法内部是怎么实现的?代码如下所示:

go 复制代码
// gin v1.10.0 context.go

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

ShouldBindWith方法调用了接口binding.BindingBind(...)方法,我们先看一下这个接口的定义:

go 复制代码
// gin v1.10.0 binding/binding.go

// 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, any) error
}

然后我们接着看一下ShouldBindJSON(...)方法中传递的binding.JSON,如下所示:

go 复制代码
// gin v1.10.0 binding/binding.go

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
	JSON          BindingBody = jsonBinding{}
	XML           BindingBody = xmlBinding{}
	Form          Binding     = formBinding{}
	Query         Binding     = queryBinding{}
	FormPost      Binding     = formPostBinding{}
	FormMultipart Binding     = formMultipartBinding{}
	ProtoBuf      BindingBody = protobufBinding{}
	MsgPack       BindingBody = msgpackBinding{}
	YAML          BindingBody = yamlBinding{}
	Uri           BindingUri  = uriBinding{}
	Header        Binding     = headerBinding{}
	TOML          BindingBody = tomlBinding{}
)

可以看到在gin中定义了很多的binding.Binding接口的实现,其中我们重点关注jsonBinding类型,代码如下:

go 复制代码
// gin v1.10.0 binding/json.go

type jsonBinding struct{}

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

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

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

简单总结一下上述代码,就是调用golang自带库的json包工具将请求体中的json数据解析成结构体数据。

总结

gin中绑定请求体中数据的方法有两大类共四个方法,分别是强制绑定的Bind(...)BindJson(...),以及非强制绑定的ShouldBind(...)ShouldBindJson(...)

我们详细分析了ShouldBindJson(...)方法,它的内部就是调用go自带库中的json包进行JSON请求数据的解析。

参考文章:

1、Gin 源码学习(二)丨请求体中的参数是如何解析的?

相关推荐
SuperEugene1 天前
TypeScript+Vue 实战:告别 any 滥用,统一接口 / Props / 表单类型,实现类型安全|编码语法规范篇
开发语言·前端·javascript·vue.js·安全·typescript
我是永恒1 天前
上架一个跨境工具导航网站
前端
电子羊1 天前
Spec 编程工作流文档
前端
GISer_Jing1 天前
从CLI到GUI桌面应用——前端工程化进阶之路
前端·人工智能·aigc·交互
还是大剑师兰特1 天前
Vue3 报错:computed value is readonly 解决方案
前端·vue.js
leaves falling1 天前
有效的字母异位词
java·服务器·前端
We་ct1 天前
LeetCode 35. 搜索插入位置:二分查找的经典应用
前端·算法·leetcode·typescript·个人开发
左耳咚1 天前
Claude Code 中的 SubAgent
前端·ai编程·claude
FPGA小迷弟1 天前
高频时钟设计:FPGA 多时钟域同步与时序收敛实战方案
前端·学习·fpga开发·verilog·fpga
IT古董1 天前
【前端】企业级前端调试体系设计(含日志埋点 + Eruda 动态注入 + Sentry)
前端·sentry