Gin 源码学习(一)丨请求中 URL 的参数是如何解析的?

2024-09-04 16:38

本文主要是介绍Gin 源码学习(一)丨请求中 URL 的参数是如何解析的?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 
参考视频教程:  
 **【Go语言中文网】资深Go开发工程师第二期  **

If you need performance and good productivity, you will love Gin.

这是 Gin 源码学习的第一篇,为什么是 Gin 呢?

正如 Gin 官方文档中所说,Gin 是一个注重性能和生产的 web 框架,并且号称其性能要比 httprouter 快近40倍,这是选择 Gin 作为源码学习的理由之一,因为其注重性能;其次是 Go 自带函数库中的 net 库和 context 库,如果要说为什么 Go 能在国内这么火热,那么原因肯定和 net 库和 context 库有关,所以本系列的文章将借由 net 库和 context 库在 Gin 中的运用,顺势对这两个库进行讲解。

本系列的文章将由浅入深,从简单到复杂,在讲解 Gin 源代码的过程中结合 Go 自带函数库,对 Go 自带函数库中某些巧妙设计进行讲解。

下面开始 Gin 源码学习的第一篇:请求中 URL 的参数是如何解析的?

目录

  • 路径中的参数解析
  • 查询字符串的参数解析
  • 总结

路径中的参数解析


func main() {router := gin.Default()router.GET("/user/:name/*action", func(c *gin.Context) {name := c.Param("name")action := c.Param("action")c.String(http.StatusOK, "%s is %s", name, action)})router.Run(":8000")
}

引用 Gin 官方文档中的一个例子,我们把关注点放在 c.Param(key) 函数上面。

当发起 URI 为 /user/cole/send 的 GET 请求时,得到的响应体如下:


cole is /send

而发起 URI 为 /user/cole/ 的 GET 请求时,得到的响应体如下:


cole is /

在 Gin 内部,是如何处理做到的呢?我们先来观察 gin.Context 的内部函数 Param(),其源代码如下:


// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string {return c.Params.ByName(key)
}

从源代码的注释中可以知道,c.Param(key) 函数实际上只是 c.Params.ByName() 函数的一个捷径,那么我们再来观察一下 c.Params 属性及其类型究竟是何方神圣,其源代码如下:


// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {Params Params
}// Param is a single URL parameter, consisting of a key and a value.
type Param struct {Key   stringValue string
}// Params is a Param-slice, as returned by the router.
// The slice is ordered, the first URL parameter is also the first slice value.
// It is therefore safe to read values by the index.
type Params []Param

首先,Paramsgin.Context 类型中的一个参数(上面源代码中省略部分属性),gin.Context 是 Gin 中最重要的部分,其作用类似于 Go 自带库中的 context 库,在本系列后续的文章中会分别对各自进行讲解。

接着,Params 类型是一个由 router 返回的 Param 切片,同时,该切片是有序的,第一个 URL 参数也是切片的第一个值,而 Param 类型是由 KeyValue 组成的,用于表示 URL 中的参数。

所以,上面获取 URL 中的 name 参数和 action 参数,也可以使用以下方式获取:


name := c.Params[0].Value
action := c.Params[1].Value

而这些并不是我们所关心的,我们想知道的问题是 Gin 内部是如何把 URL 中的参数给传递到 c.Params 中的?先看以下下方的这段代码:


func main() {router := gin.Default()router.GET("/aa", func(c *gin.Context) {})router.GET("/bb", func(c *gin.Context) {})router.GET("/u", func(c *gin.Context) {})router.GET("/up", func(c *gin.Context) {})router.POST("/cc", func(c *gin.Context) {})router.POST("/dd", func(c *gin.Context) {})router.POST("/e", func(c *gin.Context) {})router.POST("/ep", func(c *gin.Context) {})// http://127.0.0.1:8000/user/cole/send => cole is /send// http://127.0.0.1:8000/user/cole/ => cole is /router.GET("/user/:name/*action", func(c *gin.Context) {// name := c.Param("name")// action := c.Param("action")name := c.Params[0].Valueaction := c.Params[1].Valuec.String(http.StatusOK, "%s is %s", name, action)})router.Run(":8000")
}

把关注点放在路由的绑定上,这段代码保留了最开始的那个 GET 路由,并且另外创建了 4 个 GET 路由和 4 个 POST 路由,在 Gin 内部,将会生成类似下图所示的路由树。

路由树.jpg

当然,请求 URL 是如何匹配的问题也不是本文要关注的,在后续的文章中将会对其进行详细讲解,在这里,我们需要关注的是节点中 wildChild 属性值为 true 的节点。结合上图,看一下下面的代码(为了突出重点,省略部分源代码):


func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.Request.URL.Pathunescape := false......// Find root of the tree for the given HTTP methodt := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.Params, unescape)if value.handlers != nil {c.handlers = value.handlersc.Params = value.paramsc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}......}......
}

首先,是获取请求的方法以及请求的 URL 路径,以上述的 http://127.0.0.1:8000/user/cole/send 请求为例,httpMethodrPath 分别为 GET/user/cole/send

然后,使用 engine.trees 获取路由树切片(如上路由树图的最上方),并通过 for 循环遍历该切片,找到类型与 httpMethod 相同的路由树的根节点。

最后,调用根节点的 getValue(path, po, unescape) 函数,返回一个 nodeValue 类型的对象,将该对象中的 params 属性值赋给 c.Params

好了,我们的关注点,已经转移到了 getValue(path, po, unescape) 函数,unescape 参数用于标记是否转义处理,在这里先将其忽略,下面源代码展示了在 getValue(path, po, unescape) 函数中解析 URL 参数的过程,同样地,只保留了与本文内容相关的源代码:


func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {value.params = po
walk: // Outer loop for walking the treefor {if len(path) > len(n.path) {if path[:len(n.path)] == n.path {path = path[len(n.path):]// 从根往下匹配, 找到节点中wildChild属性为true的节点if !n.wildChild {c := path[0]for i := 0; i < len(n.indices); i++ {if c == n.indices[i] {n = n.children[i]continue walk}}......return}// handle wildcard childn = n.children[0]// 匹配两种节点类型: param和catchAll// 可简单理解为:// 节点的path值为':xxx', 则节点为param类型节点// 节点的path值为'/*xxx', 则节点为catchAll类型节点switch n.nType {case param:// find param end (either '/' or path end)end := 0for end < len(path) && path[end] != '/' {end++}// save param valueif cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // expand slice within preallocated capacityvalue.params[i].Key = n.path[1:]val := path[:end]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(val); err != nil {value.params[i].Value = val // fallback, in case of error}} else {value.params[i].Value = val}// we need to go deeper!if end < len(path) {if len(n.children) > 0 {path = path[end:]n = n.children[0]continue walk}...return}......returncase catchAll:// save param valueif cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // expand slice within preallocated capacityvalue.params[i].Key = n.path[2:]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(path); err != nil {value.params[i].Value = path // fallback, in case of error}} else {value.params[i].Value = path}returndefault:panic("invalid node type")}}}......return}
}

首先,会通过 path 在路由树中进行匹配,找到节点中 wildChild 值为 true 的节点,表示该节点的孩子节点为通配符节点,然后获取该节点的孩子节点。

然后,通过 switch 判断该通配符节点的类型,若为 param,则进行截取,获取参数的 Key 和 Value,并放入 value.params 中;若为 catchAll,则无需截取,直接获取参数的 Key 和 Value,放入 value.params 中即可。其中 n.maxParams 属性在创建路由时赋值,也不是这里需要关注的内容,在本系列的后续文章中讲会涉及。

上述代码中,比较绕的部分主要为节点的匹配,可结合上面给出的路由树图观看,方便理解,同时,也省略了部分与我们目的无关的源代码,相信要看懂上述给出的源代码,应该并不困难。

查询字符串的参数解析


func main() {router := gin.Default()// http://127.0.0.1:8000/welcome?firstname=Les&lastname=An => Hello Les Anrouter.GET("/welcome", func(c *gin.Context) {firstname := c.DefaultQuery("firstname", "Guest")lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")c.String(http.StatusOK, "Hello %s %s", firstname, lastname)})router.Run(":8080")
}

同样地,引用 Gin 官方文档中的例子,我们把关注点放在 c.DefaultQuery(key, defaultValue)c.Query(key) 上,当然,这俩其实没啥区别。

当发起 URI 为 /welcome?firstname=Les&lastname=An 的 GET 请求时,得到的响应体结果如下:


Hello Les An

接下来,看一下 c.DefaultQuery(key, defaultValue)c.Query(key) 的源代码:


// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /path?id=1234&name=Manu&value=
//     c.Query("id") == "1234"
//     c.Query("name") == "Manu"
//     c.Query("value") == ""
//     c.Query("wtf") == ""
func (c *Context) Query(key string) string {value, _ := c.GetQuery(key)return value
}// DefaultQuery returns the keyed url query value if it exists,
// otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information.
//     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 {if value, ok := c.GetQuery(key); ok {return value}return defaultValue
}

从上述源代码中可以发现,两者都调用了 c.GetQuery(key) 函数,接下来,我们来跟踪一下源代码:


// GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string),
// otherwise it returns `("", false)`.
// It is shortcut for `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) {if values, ok := c.GetQueryArray(key); ok {return values[0], ok}return "", false
}// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {c.getQueryCache()if values, ok := c.queryCache[key]; ok && len(values) > 0 {return values, true}return []string{}, false
}

c.GetQuery(key) 函数内部调用了 c.GetQueryArray(key) 函数,而在 c.GetQueryArray(key) 函数中,先是调用了 c.getQueryCache() 函数,之后即可通过 key 直接从 c.queryCache 中获取对应的 value 值,基本上可以确定 c.getQueryCache() 函数的作用就是把查询字符串参数存储到 c.queryCache 中。下面,我们来看一下c.getQueryCache() 函数的源代码:


func (c *Context) getQueryCache() {if c.queryCache == nil {c.queryCache = c.Request.URL.Query()}
}

先是判断 c.queryCache 的值是否为 nil,如果为 nil,则调用 c.Request.URL.Query() 函数;否则,不做处理。

我们把关注点放在 c.Request 上面,其为 *http.Request 类型,位于 Go 自带函数库中的 net/http 库,而 c.Request.URL 则位于 Go 自带函数库中的 net/url 库,表明接下来的源代码来自 Go 自带函数库中,我们来跟踪一下源代码:


// Query parses RawQuery and returns the corresponding values.
// It silently discards malformed value pairs.
// To check errors use ParseQuery.
func (u *URL) Query() Values {v, _ := ParseQuery(u.RawQuery)return v
}// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string// ParseQuery parses the URL-encoded query string and returns
// a map listing the values specified for each key.
// ParseQuery always returns a non-nil map containing all the
// valid query parameters found; err describes the first decoding error
// encountered, if any.
//
// Query is expected to be a list of key=value settings separated by
// ampersands or semicolons. A setting without an equals sign is
// interpreted as a key set to an empty value.
func ParseQuery(query string) (Values, error) {m := make(Values)err := parseQuery(m, query)return m, err
}func parseQuery(m Values, query string) (err error) {for query != "" {key := query// 如果key中存在'&'或者';', 则用其对key进行分割// 例如切割前: key = firstname=Les&lastname=An// 例如切割后: key = firstname=Les, query = lastname=Anif i := strings.IndexAny(key, "&;"); i >= 0 {key, query = key[:i], key[i+1:]} else {query = ""}if key == "" {continue}value := ""// 如果key中存在'=', 则用其对key进行分割// 例如切割前: key = firstname=Les// 例如切割后: key = firstname, value = Lesif i := strings.Index(key, "="); i >= 0 {key, value = key[:i], key[i+1:]}// 对key进行转义处理key, err1 := QueryUnescape(key)if err1 != nil {if err == nil {err = err1}continue}// 对value进行转义处理value, err1 = QueryUnescape(value)if err1 != nil {if err == nil {err = err1}continue}// 将value追加至m[key]切片中m[key] = append(m[key], value)}return err
}

首先是 u.Query() 函数,通过解析 RawQuery 的值,以上面 GET 请求为例,则其 RawQuery 值为 firstname=Les&lastname=An,返回值为一个 Values 类型的对象,Values 为一个 key 类型为字符串,value 类型为字符串切片的 map。

然后是 ParseQuery(query) 函数,在该函数中创建了一个 Values 类型的对象 m,并用其和传递进来的 query 作为 parseQuery(m, query) 函数的参数。

最后在 parseQuery(m, query) 函数内将 query 解析至 m中,至此,查询字符串参数解析完毕。

总结

这篇文章讲解了 Gin 中的 URL 参数解析的两种方式,分别是路径中的参数解析和查询字符串的参数解析。

其中,路径中的参数解析过程结合了 Gin 中的路由匹配机制,由于路由匹配机制的巧妙设计,使得这种方式的参数解析非常高效,当然,路由匹配机制稍微有些许复杂,这在本系列后续的文章中将会进行详细讲解;然后是查询字符的参数解析,这种方式的参数解析与 Go 自带函数库 net/url 库的区别就是,Gin 将解析后的参数保存在了上下文中,这样的话,对于获取多个参数时,则无需对查询字符串进行重复解析,使获取多个参数时的效率提高了不少,这也是 Gin 为何效率如此之快的原因之一。

至此,本文也就结束了,感谢大家的阅读,本系列的下一篇文章将讲解 POST 请求中的表单数据在 Gin 内部是如何解析的。

这篇关于Gin 源码学习(一)丨请求中 URL 的参数是如何解析的?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1136456

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca