切换菜单
搜索
个人笔记云
首页
java
spring
springmvc
python
使用教程
笔记管理
搜索
登录/注册
好物分享
退出
搜索
Gin 源码学习(一)丨请求中 URL 的参数是如何解析的?
2021-10-10
672
**参考视频教程:** [**【Go语言中文网】资深Go开发工程师第二期 **](http://www.notescloud.top/goods/detail/1342) > 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 的参数是如何解析的? 目录 --- * [路径中的参数解析](#%E8%B7%AF%E5%BE%84%E4%B8%AD%E7%9A%84%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90) * [查询字符串的参数解析](#%E6%9F%A5%E8%AF%A2%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90) * [总结](#%E6%80%BB%E7%BB%93) ### 路径中的参数解析 ``` 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 string Value 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 ``` 首先,`Params` 是 `gin.Context` 类型中的一个参数(上面源代码中省略部分属性),`gin.Context` 是 Gin 中最重要的部分,其作用类似于 Go 自带库中的 `context` 库,在本系列后续的文章中会分别对各自进行讲解。 接着,`Params` 类型是一个由 `router` 返回的 `Param` 切片,同时,该切片是有序的,第一个 URL 参数也是切片的第一个值,而 `Param` 类型是由 `Key` 和 `Value` 组成的,用于表示 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].Value action := c.Params[1].Value c.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.Method rPath := c.Request.URL.Path unescape := false ... ... // Find root of the tree for the given HTTP method t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root // Find route in tree value := root.getValue(rPath, c.Params, unescape) if value.handlers != nil { c.handlers = value.handlers c.Params = value.params c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } ... ... } ... ... } ``` 首先,是获取请求的方法以及请求的 URL 路径,以上述的 `http://127.0.0.1:8000/user/cole/send` 请求为例,`httpMethod` 和 `rPath` 分别为 `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 tree for { 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 child n = n.children[0] // 匹配两种节点类型: param和catchAll // 可简单理解为: // 节点的path值为':xxx', 则节点为param类型节点 // 节点的path值为'/*xxx', 则节点为catchAll类型节点 switch n.nType { case param: // find param end (either '/' or path end) end := 0 for end < len(path) && path[end] != '/' { end++ } // save param value if 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 capacity value.params[i].Key = n.path[1:] val := path[:end] if unescape { var err error if 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 } ... ... return case catchAll: // save param value if 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 capacity value.params[i].Key = n.path[2:] if unescape { var err error if 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 } return default: 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 An router.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=An if 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 = Les if 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 内部是如何解析的。
教程分类
热门视频教程
热门文章
热门书籍推荐