本文主要是介绍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
首先,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].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
请求为例,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 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 的参数是如何解析的?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!