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
複製代碼
首先,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 內部,將會生成相似下圖所示的路由樹。
固然,請求 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 內部是如何解析的。