Gin 源碼學習(一)丨請求中 URL 的參數是如何解析的?

Gin 源碼學習(一)丨請求中 URL 的參數是如何解析的?

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

這是 Gin 源碼學習的第一篇,爲何是 Gin 呢?web

正如 Gin 官方文檔中所說,Gin 是一個注重性能和生產的 web 框架,而且號稱其性能要比 httprouter 快近40倍,這是選擇 Gin 做爲源碼學習的理由之一,由於其注重性能;其次是 Go 自帶函數庫中的 net 庫和 context 庫,若是要說爲何 Go 能在國內這麼火熱,那麼緣由確定和 net 庫和 context 庫有關,因此本系列的文章將藉由 net 庫和 context 庫在 Gin 中的運用,順勢對這兩個庫進行講解。bash

本系列的文章將由淺入深,從簡單到複雜,在講解 Gin 源代碼的過程當中結合 Go 自帶函數庫,對 Go 自帶函數庫中某些巧妙設計進行講解。app

下面開始 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 請求時,獲得的響應體以下:oop

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 屬性及其類型到底是何方神聖,其源代碼以下:ui

// 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
複製代碼

首先,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].Value
		action := c.Params[1].Value
		c.String(http.StatusOK, "%s is %s", name, action)
	})

	router.Run(":8000")
}
複製代碼

把關注點放在路由的綁定上,這段代碼保留了最開始的那個 GET 路由,而且另外建立了 4 個 GET 路由和 4 個 POST 路由,在 Gin 內部,將會生成相似下圖所示的路由樹。

固然,請求 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 請求爲例,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 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 內部是如何解析的。

相關文章
相關標籤/搜索