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

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

這是 Gin 源碼學習的第一篇,為什么是 Gin 呢?

正如 Gin 官方文檔中所說,Gin 是一個注重性能和生產(chǎn)的 web 框架,并且號稱其性能要比 httprouter 快近40倍,這是選擇 Gin 作為源碼學習的理由之一,因為其注重性能;其次是 Go 自帶函數(shù)庫中的 net 庫和 context 庫,如果要說為什么 Go 能在國內(nèi)這么火熱,那么原因肯定和 net 庫和 context 庫有關(guān),所以本系列的文章將借由 net 庫和 context 庫在 Gin 中的運用,順勢對這兩個庫進行講解。

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

下面開始 Gin 源碼學習的第一篇:請求中 URL 的參數(shù)是如何解析的?

目錄

路徑中的參數(shù)解析

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 官方文檔中的一個例子,我們把關(guān)注點放在 c.Param(key) 函數(shù)上面。

當發(fā)起 URI 為 /user/cole/send 的 GET 請求時,得到的響應體如下:

cole is /send

而發(fā)起 URI 為 /user/cole/ 的 GET 請求時,得到的響應體如下:

cole is /

在 Gin 內(nèi)部,是如何處理做到的呢?我們先來觀察 gin.Context 的內(nèi)部函數(shù) 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) 函數(shù)實際上只是 c.Params.ByName() 函數(shù)的一個捷徑,那么我們再來觀察一下 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

首先,Paramsgin.Context 類型中的一個參數(shù)(上面源代碼中省略部分屬性),gin.Context 是 Gin 中最重要的部分,其作用類似于 Go 自帶庫中的 context 庫,在本系列后續(xù)的文章中會分別對各自進行講解。

接著,Params 類型是一個由 router 返回的 Param 切片,同時,該切片是有序的,第一個 URL 參數(shù)也是切片的第一個值,而 Param 類型是由 KeyValue 組成的,用于表示 URL 中的參數(shù)。

所以,上面獲取 URL 中的 name 參數(shù)和 action 參數(shù),也可以使用以下方式獲?。?/p>

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

而這些并不是我們所關(guān)心的,我們想知道的問題是 Gin 內(nèi)部是如何把 URL 中的參數(shù)給傳遞到 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")
}

把關(guān)注點放在路由的綁定上,這段代碼保留了最開始的那個 GET 路由,并且另外創(chuàng)建了 4 個 GET 路由和 4 個 POST 路由,在 Gin 內(nèi)部,將會生成類似下圖所示的路由樹。

路由樹.jpg

當然,請求 URL 是如何匹配的問題也不是本文要關(guān)注的,在后續(xù)的文章中將會對其進行詳細講解,在這里,我們需要關(guān)注的是節(jié)點中 wildChild 屬性值為 true 的節(jié)點。結(jié)合上圖,看一下下面的代碼(為了突出重點,省略部分源代碼):

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 循環(huán)遍歷該切片,找到類型與 httpMethod 相同的路由樹的根節(jié)點。

最后,調(diào)用根節(jié)點的 getValue(path, po, unescape) 函數(shù),返回一個 nodeValue 類型的對象,將該對象中的 params 屬性值賦給 c.Params

好了,我們的關(guān)注點,已經(jīng)轉(zhuǎn)移到了 getValue(path, po, unescape) 函數(shù),unescape 參數(shù)用于標記是否轉(zhuǎn)義處理,在這里先將其忽略,下面源代碼展示了在 getValue(path, po, unescape) 函數(shù)中解析 URL 參數(shù)的過程,同樣地,只保留了與本文內(nèi)容相關(guān)的源代碼:

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):]
                // 從根往下匹配, 找到節(jié)點中wildChild屬性為true的節(jié)點
                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]
                // 匹配兩種節(jié)點類型: param和catchAll
                // 可簡單理解為:
                // 節(jié)點的path值為':xxx', 則節(jié)點為param類型節(jié)點
                // 節(jié)點的path值為'/*xxx', 則節(jié)點為catchAll類型節(jié)點
                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 在路由樹中進行匹配,找到節(jié)點中 wildChild 值為 true 的節(jié)點,表示該節(jié)點的孩子節(jié)點為通配符節(jié)點,然后獲取該節(jié)點的孩子節(jié)點。

然后,通過 switch 判斷該通配符節(jié)點的類型,若為 param,則進行截取,獲取參數(shù)的 Key 和 Value,并放入 value.params 中;若為 catchAll,則無需截取,直接獲取參數(shù)的 Key 和 Value,放入 value.params 中即可。其中 n.maxParams 屬性在創(chuàng)建路由時賦值,也不是這里需要關(guān)注的內(nèi)容,在本系列的后續(xù)文章中講會涉及。

上述代碼中,比較繞的部分主要為節(jié)點的匹配,可結(jié)合上面給出的路由樹圖觀看,方便理解,同時,也省略了部分與我們目的無關(guān)的源代碼,相信要看懂上述給出的源代碼,應該并不困難。

查詢字符串的參數(shù)解析

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 官方文檔中的例子,我們把關(guān)注點放在 c.DefaultQuery(key, defaultValue)c.Query(key) 上,當然,這倆其實沒啥區(qū)別。

當發(fā)起 URI 為 /welcome?firstname=Les&lastname=An 的 GET 請求時,得到的響應體結(jié)果如下:

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
}

從上述源代碼中可以發(fā)現(xiàn),兩者都調(diào)用了 c.GetQuery(key) 函數(shù),接下來,我們來跟蹤一下源代碼:

// 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) 函數(shù)內(nèi)部調(diào)用了 c.GetQueryArray(key) 函數(shù),而在 c.GetQueryArray(key) 函數(shù)中,先是調(diào)用了 c.getQueryCache() 函數(shù),之后即可通過 key 直接從 c.queryCache 中獲取對應的 value 值,基本上可以確定 c.getQueryCache() 函數(shù)的作用就是把查詢字符串參數(shù)存儲到 c.queryCache 中。下面,我們來看一下c.getQueryCache() 函數(shù)的源代碼:

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

先是判斷 c.queryCache 的值是否為 nil,如果為 nil,則調(diào)用 c.Request.URL.Query() 函數(shù);否則,不做處理。

我們把關(guān)注點放在 c.Request 上面,其為 *http.Request 類型,位于 Go 自帶函數(shù)庫中的 net/http 庫,而 c.Request.URL 則位于 Go 自帶函數(shù)庫中的 net/url 庫,表明接下來的源代碼來自 Go 自帶函數(shù)庫中,我們來跟蹤一下源代碼:

// 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進行轉(zhuǎn)義處理
        key, err1 := QueryUnescape(key)
        if err1 != nil {
            if err == nil {
                err = err1
            }
            continue
        }
        // 對value進行轉(zhuǎn)義處理
        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() 函數(shù),通過解析 RawQuery 的值,以上面 GET 請求為例,則其 RawQuery 值為 firstname=Les&lastname=An,返回值為一個 Values 類型的對象,Values 為一個 key 類型為字符串,value 類型為字符串切片的 map。

然后是 ParseQuery(query) 函數(shù),在該函數(shù)中創(chuàng)建了一個 Values 類型的對象 m,并用其和傳遞進來的 query 作為 parseQuery(m, query) 函數(shù)的參數(shù)。

最后在 parseQuery(m, query) 函數(shù)內(nèi)將 query 解析至 m中,至此,查詢字符串參數(shù)解析完畢。

總結(jié)

這篇文章講解了 Gin 中的 URL 參數(shù)解析的兩種方式,分別是路徑中的參數(shù)解析和查詢字符串的參數(shù)解析。

其中,路徑中的參數(shù)解析過程結(jié)合了 Gin 中的路由匹配機制,由于路由匹配機制的巧妙設(shè)計,使得這種方式的參數(shù)解析非常高效,當然,路由匹配機制稍微有些許復雜,這在本系列后續(xù)的文章中將會進行詳細講解;然后是查詢字符的參數(shù)解析,這種方式的參數(shù)解析與 Go 自帶函數(shù)庫 net/url 庫的區(qū)別就是,Gin 將解析后的參數(shù)保存在了上下文中,這樣的話,對于獲取多個參數(shù)時,則無需對查詢字符串進行重復解析,使獲取多個參數(shù)時的效率提高了不少,這也是 Gin 為何效率如此之快的原因之一。

至此,本文也就結(jié)束了,感謝大家的閱讀,本系列的下一篇文章將講解 POST 請求中的表單數(shù)據(jù)在 Gin 內(nèi)部是如何解析的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。