httprouter 原始碼分析

broqiang發表於2019-04-17

關於 httprouter 本身就不過多說了,可以直接去檢視原始碼及 README 。

這個包相對還是比較簡單了,只有幾個檔案,並且除了標準庫沒有外部的依賴。難理解的就是基數樹,需要演算法基礎。

拋磚引玉,有不對的地方望指出,我及時修改。

入口

使用的是程式碼追蹤的方式,可以從官方給的 demo 來入手:

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}

可以看到,這個 demo 還是比較簡單的, 主要就做了幾件事:

  • 定義了兩個業務處理的函式(雖然只做了簡單的字串輸出)。

  • 初始化路由, httprouter.New()

  • 將上面的兩個函式註冊到路由中

  • 使用 httprouter 的 handler 啟動服務

下面就從 main 函式開始看,通過 New() 函式初始化了一下 httprouter ,然後呼叫了兩次 GET 方法,分別傳入了 Index 和 Hello 的業務處理函式。

下面就追蹤到 New() 中,看看它到底做了什麼,這個函式也很簡單, 只是初始化了一下 Router 結構,將幾個引數的預設值設定為 true ,並返回了 *Router (指標)。

func New() *Router {
    return &Router{
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      true,
        HandleMethodNotAllowed: true,
        HandleOPTIONS:          true,
    }
}

這個 Route 是什麼? 裡面的引數又代表了什麼,追蹤到 Router 去看一下。

Router struct

先看看這個結構體裡面包含了什麼欄位,順便將註釋翻譯一下,這裡就沒有保留註釋的原文,如果想要了解原文,可以直接去 原始碼 中檢視即可。

// Router 是一個 http.Handler 可以通過定義的路由將請求分發給不同的函式
type Router struct {
    trees map[string]*node

    // 這個引數是否自動處理當訪問路徑最後帶的 /,一般為 true 就行。
    // 例如: 當訪問 /foo/ 時, 此時沒有定義 /foo/ 這個路由,但是定義了 
    // /foo 這個路由,就對自動將 /foo/ 重定向到 /foo (GET 請求
    // 是 http 301 重定向,其他方式的請求是 http 307 重定向)。
    RedirectTrailingSlash bool

    // 是否自動修正路徑, 如果路由沒有找到時,Router 會自動嘗試修復。
    // 首先刪除多餘的路徑,像 ../ 或者 // 會被刪除。
    // 然後將清理過的路徑再不區分大小寫查詢,如果能夠找到對應的路由, 將請求重定向到
    // 這個路由上 ( GET 是 301, 其他是 307 ) 。
    RedirectFixedPath bool

    // 用來配合下面的 MethodNotAllowed 引數。 
    HandleMethodNotAllowed bool

    // 如果為 true ,會自動回覆 OPTIONS 方式的請求。
    // 如果自定義了 OPTIONS 路由,會使用自定義的路由,優先順序高於這個自動回覆。
    HandleOPTIONS bool

    // 路由沒有匹配上時呼叫這個 handler 。
    // 如果沒有定義這個 handler ,就會返回標準庫中的 http.NotFound 。
    NotFound http.Handler

    // 當一個請求是不被允許的,並且上面的 HandleMethodNotAllowed 設定為 ture 的時候,
    // 如果這個引數沒有設定,將使用狀態為 with http.StatusMethodNotAllowed 的 http.Error
    // 在 handler 被呼叫以前,為允許請求的方法設定 "Allow" header 。
    MethodNotAllowed http.Handler

    // 當出現 panic 的時候,通過這個函式來恢復。會返回一個錯誤碼為 500 的 http error 
    // (Internal Server Error) ,這個函式是用來保證出現 painc 伺服器不會崩潰。
    PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

現在可以看到, Router 這個結構就是 httprouter 的一個核心的部分,這裡定義了路由的一些初始配置,基本通過註釋就可以知道它們是做什麼用的,上面還有個 trees 是這個結構最核心的內容,竟然沒註釋(原文就沒有註釋),這裡面儲存的就是註冊的路由,按照 method (POST,GET ...) 方法分開,每一個方法對應一個基數樹(radix tree)。這個樹比較複雜,暫時先不去理會,簡單的理解成將路由儲存進來即可。

在 Router 結構和 New() 函式之間還有一行比較有意思的寫法:

var _ http.Handler = New()

這個其實就是通過 New() 函式初始化一個 Router ,並且指定 Router 實現了 http.Handler 介面 ,如果 New() 沒有實現 http.Handler 介面,在編譯的時候就會報錯了。這裡只是為了驗證一下, New() 函式的返回值並不需要,所以就把它賦值給 _ ,相當於是給丟棄了。

到這裡,我們就可以看到了, Router 也是基於 http.Handler 做的實現,如果要實現 http.Handler 介面,就必須實現 ServeHTTP(w http.ResponseWriter, req *http.Request) 這個方法,下面就可以去追蹤下 ServerHTTP 都做了什麼。

ServerHTTP

這個程式碼比較長,就將分析的步驟寫在註釋中了,關於基數樹放在最後說明。

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 在最開始,就做了 panic 的處理,這樣可以保證業務程式碼出現問題的時候,不會導致伺服器崩潰
    // 這裡就是簡單的在瀏覽器返回一個 500 錯誤,如果想在 panic 的時候自己處理
    // 只需要將 r.PanicHandler = func(http.ResponseWriter, *http.Request, interface{}) 
    // 重寫, 新增上自己的處理邏輯即可。
    if r.PanicHandler != nil {
        defer r.recv(w, req)
    }

    // 通過 Request 獲取當前請求的路徑
    path := req.URL.Path

    // 到基數樹中去查詢匹配的路由
    // 首先看是否有請求的方法(比如 GET)是否存在路由,如果存在就繼續尋找
    if root := r.trees[req.Method]; root != nil {

        // 如果路由匹配上了,從基數樹中將路由取出
        if handle, ps, tsr := root.getValue(path); handle != nil {
            // 獲取的是一個函式,簽名 func(http.ResponseWriter, *http.Request, Params)
            // 這個就是我們向 httprouter 註冊的函式
            handle(w, req, ps)
            // 處理完成後 return ,此次生命週期就結束了
            return

            // 當沒有找到的時候,並且請求的方法不是 CONNECT 並且 路徑不是 / 的時候 
        } else if req.Method != "CONNECT" && path != "/" {
            // 這裡就要做重定向處理, 預設是 301
            code := 301 // Permanent redirect, request with GET method

            // 如果請求的方式不是 GET 就將 http 的響應碼設定成 307
            if req.Method != "GET" {
                // Temporary redirect, request with same method
                // As of Go 1.3, Go does not support status code 308.
                // 看上面的註釋的意思貌似是作者想使用 308(永久重定向),但是 Go 1.3 
                // 不支援 308 ,所以使用了一個臨時重定向。 為什麼不能用 301 呢?
                // 因為 308 和 307 不允許請求方法從 POST 更改為 GET
                code = 307
            }

            // tsr 返回值是一個 bool 值,用來判斷是否需要重定向, getValue 返回來的 
            // RedirectTrailingSlash 這個就是初始化時候定義的,只有為 true 才會處理 
            if tsr && r.RedirectTrailingSlash {
                // 如果 path 的長度大於 1,只有大於 1 才會出現這種情況,如 p/,path/
                // 並且路徑的最後是 / 的時候將最後的 / 去除
                if len(path) > 1 && path[len(path)-1] == '/' {
                    req.URL.Path = path[:len(path)-1]
                } else {
                    // 如果不是 / 結尾的,給結尾新增一個 /
                    // 假設定義了一個路由 '/foo/' ,並且沒有定義路由 /foo ,
                    // 實際訪問的是 '/foo', 將 /foo 重定向到 /foo/
                    req.URL.Path = path + "/"
                }

                // 將處理過的路由重定向, 這個是一個 http 標準包裡面的方法
                http.Redirect(w, req, req.URL.String(), code)
                return
            }

            // Try to fix the request path
            // 路由沒有找到,重定向規則也不符合,這裡會嘗試修復路徑
            // 需要在初始化的時候定義 RedirectFixedPath 為 true,允許修復
            if r.RedirectFixedPath {
                // 這裡就是在處理 Router 裡面說的,將路徑通過 CleanPath 方法去除多餘的部分
                // 並且 RedirectTrailingSlash 為 ture 的時候,去匹配路由 
                // 比如: 定義了一個路由 /foo , 但實際訪問的是 ////FOO ,就會被重定向到 /foo
                fixedPath, found := root.findCaseInsensitivePath(
                    CleanPath(path),
                    r.RedirectTrailingSlash,
                )
                if found {
                    req.URL.Path = string(fixedPath)
                    http.Redirect(w, req, req.URL.String(), code)
                    return
                }
            }
        }
    }

    // 路由也沒有匹配,重定向也沒有找到,修復後仍然沒有匹配,就到了這裡

    // 如果沒有任何路由匹配,請求方式又是 OPTIONS, 並且允許響應 OPTIONS
    // 就會給 Header 設定一個 Allow 頭,返回去
    if req.Method == "OPTIONS" && r.HandleOPTIONS {
        // Handle OPTIONS requests
        if allow := r.allowed(path, req.Method); len(allow) > 0 {
          w.Header().Set("Allow", allow)
            return
        }
    } else {
        // 如果不是 OPTIONS 或者 不允許 OPTIONS 時 
        // Handle 405
        // 如果初始化的時候 HandleMethodNotAllowed 為 ture
        if r.HandleMethodNotAllowed {
            // 返回 405 響應,通過 allowed() 方法來處理 405 時 allow的值。
            // 大概意思是這樣的,比如定義了一個 POST 方法的路由 POST("/foo",...)
            // 但是呼叫卻是通過 GET 方式,這是就會給呼叫者返回一個包含 POST 的 405
            if allow := r.allowed(path, req.Method); len(allow) > 0 {
                w.Header().Set("Allow", allow)
                if r.MethodNotAllowed != nil {
                    // 這裡預設就會返回一個字串 Method Not Allowed
                    // 可以自定義 r.MethodNotAllowed = 自定義的 http.Handler 實現
                    // 來按照需求響應, allowd 方法也不太難,就是各種判斷和查詢,就不分析了。
                    r.MethodNotAllowed.ServeHTTP(w, req)
                } else {
                    http.Error(w,
                        http.StatusText(http.StatusMethodNotAllowed),
                        http.StatusMethodNotAllowed,
                    )
                }
                return
            }
        }
    }

    // Handle 404
    // 如果什麼的沒有找到,就只會返回一個 404 了
    // 如果定義了 NotFound ,就會呼叫自定義的
    if r.NotFound != nil {
        // 如果需要自定義,在初始化之後給 NotFound 賦一個值就可以了。
        // 可以簡單的通過 http.HandlerFunc 包裝一個 handler ,例如:
        // router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        //         w.Write([]byte("什麼都沒有找到"))
        // })
        // 
        r.NotFound.ServeHTTP(w, req)
    } else {
        // 如果沒有定義,就返回一個 http 標準庫的  NotFound 函式,返回一個 404 page not found
        http.NotFound(w, req)
    }

到這裡基本也就清除了它的基本呼叫的流程, 就是定義一個 Router 結構,並且使它實現 http.Handler 介面, 也就是新增一個 ServeHTTP 方法。 然後在 ServeHTTP 中去做路由的處理。

路由設定

現在為止,看到的都是路由的呼叫,那些路由又是哪裡來的呢? 其實如果看了 router.go 這個檔案,就可以在裡面發現,GET, POST, PUT 等方法,就是這些方法來設定的。

還記得開始的時候,引用了一個官方的 demo,裡面就有兩個設定路由的例子:

...

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

...

router.GET("/", Index)

下面就追蹤這個 GET 方法,看看它到底做了什麼:

func (r *Router) GET(path string, handle Handle) {
    r.Handle("GET", path, handle)
}

...

// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func (r *Router) DELETE(path string, handle Handle) {
    r.Handle("DELETE", path, handle)
}

...

可以看到,從 GET 方法一直到 DELETE 方法,幾乎長得都一樣,也沒做其它事情,就是呼叫了一下 Handle 方法,繼續追蹤 Handle 方法。

// 通過給定的路徑和方法,註冊一個新的 handle 請求,對於 GET, POST, PUT, PATCH 和 DELETE
// 請求,相對應的方法,直接呼叫這個方法也是可以的,只需要多傳入第一個引數即可。
// 官方給的建議是: 在批量載入或使用非標準的自定義方法時候使用。
// 
// 它有三個引數,第一個是請求方式(GET,POST……), 第二個是路由的路徑, 前兩個引數都是字串型別
// 第三個引數就是我們要註冊的函式,比如上面的 Index 函式,可以追蹤一下,在這個檔案的最上面
// 有 Handle 的定義,它是一個函式型別,只要和這個 Handle 的簽名一直的都可以作為引數
func (r *Router) Handle(method, path string, handle Handle) {

    // 驗證路徑是否是 / 開頭的,如: /foo 就是可以的, foo 就會 panic,編譯的時候機會出錯
    if path[0] != '/' {
        panic("path must begin with '/' in path '" + path + "'")
    }

    // 因為 map 是一個指標型別,必須初始化才可以使用,這裡做一個判斷,
    // 如果從來沒有註冊過路由,要先初始化 tress 屬性的 map
    if r.trees == nil {
        r.trees = make(map[string]*node)
    }

    // 因為路由是一個基數樹,全部是從根節點開始,如果第一次呼叫註冊方法的時候跟是不存在的,
    // 就註冊一個根節點, 這裡是每一種請求方法是一個根節點,會存在多個樹。
    root := r.trees[method]

    // 根節點存在就直接呼叫,不存在就初始化一個
    if root == nil {
        // 需要注意的是,這裡使用的 new 來初始化,所以 root 是一個指標。
        root = new(node)
        r.trees[method] = root
    }

    // 向 root 中新增路由,樹的具體操作在後面單獨去分析。
    root.addRoute(path, handle)
}

現在,路由就已經註冊,可以使用了。 看一下上面函式的第三個引數 Handle ,追蹤一下

type Handle func(http.ResponseWriter, *http.Request, Params)

可以看到,這個簽名和 http.HandlerFunc 的簽名比較像,用途其實也是一樣的,只不過多了一個 Params 引數。這個引數就是用來支援路徑中的引數繫結的, 比如: /hello/:name ,可以通過 Params.ByName("name") 來獲取路徑繫結引數的值。其實這個 Params 就是一個 Param 的切片,
也可以自己將所有的值遍歷出來,追蹤過去看一下就很容易明白了:

// Parram 是一個 URL 引數,包含了一個 Key 和 一個 Value 。
// Key 就是註冊路由時候的使用的字串,如: "/hello/:name" ,這個 Key 就是 name
// Value 就是訪問路徑中獲取到的值, 如: localhost:8080/hello/BroQiang ,
// 此時 Value 就是 BroQiang
type Param struct {
    Key   string
    Value string
}

// Params 就是一個 Param 的切片,這樣就可以看出來, URL 引數可以設定多個了。
// 它是在 tree 的 GetValue() 方法呼叫的時候設定的,一會分析樹的時候可以看到。
// 這個切片是有順序的,第一個設定的引數就是切片的第一個值,所以通過索引獲取值是安全的
type Params []Param

// ByName 返回傳入的 name 的值,如果沒有找到就會返回一個空字串
// 這裡就是做了個迴圈,一擔找到就將值返回。
func (ps Params) ByName(name string) string {
    for i := range ps {
        if ps[i].Key == name {
            return ps[i].Value
        }
    }
    return ""
}

定義靜態檔案目錄

到此時,我們會發現整個 router.go 檔案貌似都已經追蹤完了,還整下兩個函式沒有提到: 和 LookupServeFiles

Lookup 暫時沒有發現哪裡用到了,一會檢視其他檔案的時候看看有沒有用到的地方。先留個坑,最後再填。

下面來看看 ServeFiles 這個方法。

// 這個是用來定義靜態檔案的,比如 js, css, 圖片等
// path 是定義的 URL 路徑,必須是 /*filepath 這種格式
// root 對應的是系統中的目錄或檔案系統,因為這個函式其實是使用的 http 標準庫中的 FileServer
// 來實現的檔案服務,所以 root 引數要傳入一個 http.FileSystem 型別的引數,可以通過 
// http.Dir("/etc/...") 這種方式轉換一下,因為 Dir 實現了 http.FileSystem 介面,
// 直接使用它來轉換就行了。 
//
// 示例: 比如定義了路由 router.ServeFiles("/public/*filepath", http.Dir("/etc"))
// 此時訪問 localhost:8080/public/passwd , 就可以將 /etc/passwd 檔案的內容顯示了
func (r *Router) ServeFiles(path string, root http.FileSystem) {
    // 就是因為這三行程式碼的處理,是驗證路徑是否 /xxx/*filepath 這種結構定義
    // 因為標準庫要求必須這樣設定路徑,這裡也就這樣了, 
    // 個人還真沒想到為什麼要這樣處理,應該有什麼意義吧
    if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
        panic("path must end with /*filepath in path '" + path + "'")
    }

    fileServer := http.FileServer(root)

    r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
        req.URL.Path = ps.ByName("filepath")
        fileServer.ServeHTTP(w, req)
    })
}

這個方法其實最終也是呼叫的標準庫,並且不太靈活,看需求,不用也是可以的,比如需要給靜態檔案設定快取,呼叫這個方法就沒法實現了,不過可以自己包裝一下 http.FileServer 就可以了,詳細參考這個 Issues#40

到這裡 httprouter 的基本路由處理已經讀完,在不考慮效能的情況下,完全可以模仿這個來實現一個自己的路由了。如果是一個小的服務,沒有幾個路由,其實完全不用考慮,沒必要使用樹,直接一個 map 就可以了。標準庫的 ServeMux 中的 muxEntry 也只有簡單的兩個欄位(所以支援的功能比較少)。

Radix tree

這個就是這個路由號稱最快的關鍵部分(這個沒去核實過,也不用去糾結)。開始讀原始碼前,先了解下原理(這個就是之前一直沒去讀原始碼的原因),詳細見官方給的說明: How does it work?

這裡簡單的解讀下,路由使用了一個有共同字首的一個樹結構,這個樹就是一個壓縮字首樹( compact prefix tree ) 或者就叫基數樹( Radix tree )。也就是具有共同字首的節點擁有相同的父節點,官方給出的一個 GET 請求的例子,通過示例就比較好理解了:

圖示,直接拔過來的。

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

// 這個圖相當於註冊了下面這幾個路由
GET("/search/", func1)
GET("/support/", func2)
GET("/blog/:post/", func3)
GET("/about-us/", func4)
GET("/about-us/team/", func5)
GET("/contact/", func6)

通過上面的示例可以看出:

  • *<數字> 代表一個 handler 函式的記憶體地址(指標)

  • search 和 support 擁有共同的父節點 s ,並且 s 是沒有對應的 handle 的, 只有葉子節點(就是最後一個節點,下面沒有子節點的節點)才會註冊 handler 。

  • 從根開始,一直到葉子節點,才是路由的實際路徑。

  • 路由搜尋的順序是從上向下,從左到右的順序,為了快速找到儘可能多的路由,包含子節點越多的節點,優先順序越高。

node

大概瞭解下,開始讀程式碼,詳細去看看。一樣是先找一個入口,上面在分析的時候已經留了好幾個坑, 可以慢慢填了。

首先在 router.go 中找到

...
type Router struct {
    trees map[string]*node

    ...
}

這裡是一個 map , 每一種請求方式(GET,POST ……) 單獨管理一顆樹,官方說這樣比每個節點中去儲存方法節省空間,並且查詢的時候速度會更快。

接下來看看這個 node 是什麼:

type node struct {
    // 當前節點的 URL 路徑
    // 如上面圖中的例子的首先這裡是一個 /
    // 然後 children 中會有 path 為 [s, blog ...] 等的節點 
    // 然後 s 還有 children node [earch,upport] 等,就不再說明了
    path      string

    // 判斷當前節點路徑是不是含有引數的節點, 上圖中的 :post 的上級 blog 就是wildChild節點
    wildChild bool

    // 節點型別: static, root, param, catchAll
    // static: 靜態節點, 如上圖中的父節點 s (不包含 handler 的)
    // root: 如果插入的節點是第一個, 那麼是root節點
    // catchAll: 有*匹配的節點
    // param: 引數節點,比如上圖中的 :post 節點
    nType     nodeType

    // path 中的引數最大數量,最大隻能儲存 255 個(超過這個的情況貌似太難見到了)
    // 這裡是一個非負的 8 進位制數字,最大也只能是 255 了
    maxParams uint8

    // 和下面的 children 對應,保留的子節點的第一個字元
    // 如上圖中的 s 節點,這裡儲存的就是 eu (earch 和 upport)的首字母 
    indices   string

    // 當前節點的所有直接子節點
    children  []*node

    // 當前節點對應的 handler
    handle    Handle

    // 優先順序,查詢的時候會用到,表示當前節點加上所有子節點的數目
    priority  uint32
}

現在已經知道了 node 是用來做什麼的了,就是用來儲存樹結構的,初始化的時候是一個空的樹,沒有任何資料, 接著看看怎麼向這顆樹中新增資料。

直接讀註冊路由的時候,Handle 方法中的 root.addRoute(path, handle) 沒有去分析,現在就開始啃這個了, 追蹤過去看看:

程式碼很長,真不喜歡讀這種結構的程式碼,看起來比較累。

// addRoute 將傳入的 handle 新增到路徑中
// 需要注意,這個操作不是併發安全的!!!!
func (n *node) addRoute(path string, handle Handle) {
    fullPath := path
    // 請求到這個方法,就給當前節點的權重 + 1
    n.priority++

    // 計算傳入的路徑引數的數量
    numParams := countParams(path)

    // 如果樹不是空的
    // 判斷的條件是當前節點的 path 的字串長度和子節點的數量全部都大於 0
    // 就是說如果當前節點是一個空的節點,或者當前的節點是一個葉子節點,就直接
    // 進入 else 在當前節點下面新增子節點
    if len(n.path) > 0 || len(n.children) > 0 {
    // 定義一個 lable ,迴圈裡面可以直接 break 到這裡,適合這種巢狀的比較深的
    walk:
        for {
            // 如果傳入的節點的最大引數的數量大於當前節點記錄的數量,替換
            if numParams > n.maxParams {
                n.maxParams = numParams
            }

            // 查詢最長的共同的字首
            // 公共字首不包含 ":" 或 "*"
            i := 0

            // 將最大值設定成長度較小的路徑的長度
            max := min(len(path), len(n.path))

            // 迴圈,計算出當前節點和新增的節點共同字首的長度
            for i < max && path[i] == n.path[i] {
                i++
            }

            // 如果相同字首的長度比當前節點儲存的 path 短
            // 比如當前節點現在的 path 是 sup ,新增的節點的 path 是 search
            // 它們相同的字首就變成了 s , s 比 sup 要短,符合 if 的條件,要做處理
            if i < len(n.path) {
                // 將當前節點的屬性定義到一個子節點中,沒有註釋的屬性不變,保持原樣
                child := node{
                    // path 是當前節點的 path 去除公共字首長度的部分
                    path:      n.path[i:],
                    wildChild: n.wildChild,
                    // 將型別更改為 static ,預設的,沒有 handler 的節點
                    nType:     static,
                    indices:   n.indices,
                    children:  n.children,
                    handle:    n.handle,
                    // 權重 -1 
                    priority:  n.priority - 1,
                }

                // 遍歷當前節點的所有子節點(當前節點變成子節點之後的節點),
                // 如果最大引數數量大於當前節點的數量,更新
                for i := range child.children {
                    if child.children[i].maxParams > child.maxParams {
                        child.maxParams = child.children[i].maxParams
                    }
                }

                // 在當前節點的子節點定義為當前節點轉換後的子節點
                n.children = []*node{&child}

                // 獲取子節點的首字母,因為上面分割的時候是從 i 的位置開始分割
                // 所以 n.path[i] 可以去除子節點的首字母,理論上去 child.path[0] 也是可以的
                // 這裡的 n.path[i] 取出來的是一個 uint8 型別的數字(代表字元),
                // 先用 []byte 包裝一下數字再轉換成字串格式
                n.indices = string([]byte{n.path[i]})

                // 更新當前節點的 path 為新的公共字首
                n.path = path[:i]

                // 將 handle 設定為 nil
                n.handle = nil

                // 肯定沒有引數了,已經變成了一個沒有 handle 的節點了
                n.wildChild = false
            }

            // 將新的節點新增到此節點的子節點, 這裡是新新增節點的子節點
            if i < len(path) {
                // 擷取掉公共部分,剩餘的是子節點
                path = path[i:]

                // 如果當前路徑有引數
                // 如果進入了上面 if i < len(n.path) 這個條件,這裡就不會成立了
                // 因為上一個 if 中將 n.wildChild 重新定義成了 false 
                // 什麼情況會進入到這裡呢 ? 
                // 1. 上面的 if 不生效,也就是說不會有新的公共字首, n.path = i 的時候
                // 2. 當前節點的 path 是一個引數節點就是像這種的 :post
                // 就是定義路由時候是這種形式的: blog/:post/update
                // 
                if n.wildChild {
                    // 如果進入到了這裡,證明這是一個引數節點,類似 :post 這種
                    // 不會這個節點進行處理,直接將它的子節點賦值給當前節點
                    // 比如: :post/ ,只要是引數節點,必有子節點,哪怕是
                    // blog/:post 這種,也有一個 / 的子節點
                    n = n.children[0]

                    // 又插入了一個節點,許可權再次 + 1
                    n.priority++

                    // 更新當前節點的最大引數個數
                    if numParams > n.maxParams {
                        n.maxParams = numParams
                    }

                    // 更改方法中記錄的引數個數,個人猜測,後面的邏輯還會用到
                    numParams--

                    // 檢查萬用字元是否匹配
                    // 這裡的 path 已經變成了去除了公共字首的後面部分,比如 
                    // :post/update , 就是 /update
                    // 這裡的 n 也已經是 :post 這種的下一級的節點,比如 / 或者 /u 等等
                    // 如果新增的節點的 path >= 當前節點的 path && 
                    // 當前節點的 path 長度和新增節點的前面相同數量的字元是相等的, &&
                    if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
                        // 簡單更長的萬用字元,
                        // 當前節點的 path >= 新增節點的 path ,其實有第一個條件限制,
                        // 這裡也只有 len(n.path) == len(path) 才會成立,
                        // 就是當前節點的 path 和 新增節點的 path 相等 ||
                        // 新增節點的 path 減去當前節點的 path 之後是 /
                        // 例如: n.path = name, path = name 或 
                        // n.path = name, path = name/ 這兩種情況
                        (len(n.path) >= len(path) || path[len(n.path)] == '/') {

                        // 跳出當前迴圈,進入下一次迴圈
                        // 再次迴圈的時候 
                        // 1. if i < len(n.path) 這裡就不會再進入了,現在 i == len(n.path)
                        // 2. if n.wildChild 也不會進入了,當前節點已經在上次迴圈的時候改為 children[0]
                        continue walk
                    } else {
                        // 當不是 n.path = name, path = name/ 這兩種情況的時候,
                        // 代表萬用字元衝突了,什麼意思呢?
                        // 簡單的說就是萬用字元部分只允許定義相同的或者 / 結尾的
                        // 例如:blog/:post/update,再定義一個路由 blog/:postabc/add,
                        // 這個時候就會衝突了,是不被允許的,blog 後面只可以定義 
                        // :post 或 :post/ 這種,同一個位置不允許使用多種萬用字元
                        // 這裡的處理是直接 panic 了,如果想要支援,可以嘗試重寫下面部分程式碼

                        // 下面做的事情就是組合 panic 用到的提示資訊
                        var pathSeg string

                        // 如果當前節點的型別是有*匹配的節點
                        if n.nType == catchAll {
                            pathSeg = path
                        } else {
                            // 如果不是,將 path 做字串分割 
                            // 這個是通過 / 分割,最多分成兩個部分,然後取第一部分的值
                            // 例如: path = "name/hello/world"
                            // 分割兩部分就是 name 和 hello/world , pathSeg = name
                            pathSeg = strings.SplitN(path, "/", 2)[0]
                        }

                        // 通過傳入的原始路徑來處理字首, 可以到上面看下,方法進入就定義了這個變數
                        // 在原始路徑中提取出 pathSeg 前面的部分在拼接上 n.path
                        // 例如: n.path = ":post" , fullPath="/blog/:postnew/add"
                        // 這時的 prefix = "/blog/:post"
                        prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path

                        // 最終的提示資訊就會生成類似這種: 
                        // panic: ':postnew' in new path '/blog/:postnew/update/' \
                        // conflicts with existing wildcard ':post' in existing \
                        // prefix '/blog/:post'
                        // 就是說已經定義了 /blog/:post 這種規則的路由,
                        // 再定義 /blog/:postnew 這種就不被允許了
                        panic("'" + pathSeg +
                            "' in new path '" + fullPath +
                            "' conflicts with existing wildcard '" + n.path +
                            "' in existing prefix '" + prefix +
                            "'")
                    }
                }

                // 如果沒有進入到上面的引數節點,當前節點不是一個引數節點 :post 這種
                c := path[0]

                // slash after param
                // 如果當前節點是一個引數節點,如 /:post && 是 / 開頭的 && 只有一個子節點
                if n.nType == param && c == '/' && len(n.children) == 1 {
                    // /:post 這種節點不做處理,直接拿這個節點的子節點去匹配
                    n = n.children[0]
                    // 權重 + 1 , 因為新的節點會變成這個節點的子節點
                    n.priority++

                    // 結束當前迴圈,再次進行匹配
                    // 比如: /:post/add 當前節點是 /:post 有個子節點 /add
                    // 新新增的節點是 /:post/update ,再次迴圈的時候將會用
                    // /add 和 /update 進行匹配, 就相當於是兩次新的匹配,
                    // 由於 node 本身是一個指標,所以它們一直都會在 /:post 下面搞事情
                    continue walk
                }

                // 檢查新增的 path 的首字母是否儲存在在當前節點的 indices 中
                for i := 0; i < len(n.indices); i++ {
                    // 如果存在
                    if c == n.indices[i] {
                        // 這裡處理優先順序和排序的問題,把這個方法看完再去檢視這個方法幹了什麼
                        i = n.incrementChildPrio(i)

                        // 將當前的節點替換成它對應的子節點
                        n = n.children[i]

                        // 結束當前迴圈,繼續下一次
                        // 此時的當前 node n,已經變成了它的子位元組,下次迴圈就從這個子節點開始處理了
                        // 比如:
                        // 當前節點是 s , 包含兩個節點 earch 和 upport
                        // 這時 indices 就會有字母 e 和 u 並且是和子節點 earch 和 uppor 相對應
                        // 新新增的節點如果叫 subject , 與當前節點匹配去除公共字首 s 後, 就變成了
                        // ubject ,這時 n = upport 這個節點了,path = ubject 
                        // 下一次迴圈就拿 upport 這個 n 和 ubject 這個 path 去開始下次的匹配
                        continue walk
                    }
                }

                // 如果上面 for 中也沒有匹配上,就將新新增的節點插入

                // 如果第一個字元不是萬用字元 : 並且不是 *
                if c != ':' && c != '*' {
                    // []byte for proper unicode char conversion, see #65
                    // 將 path 的首字母 c 拼接到 indices 中
                    n.indices += string([]byte{c})

                    // 初始化一個新的節點
                    child := &node{
                        maxParams: numParams,
                    }

                    // 將這個新初始化的節點新增到當前節點的子節點中
                    n.children = append(n.children, child)

                    // 這個應該還是在處理優先順序,一會再去看這個方法
                    n.incrementChildPrio(len(n.indices) - 1)

                    // 將當前節點替換成新生成的節點
                    n = child
                }

                // 用當前節點發起插入子節點的動作
                // 注意這個 n 已經替換成了上面新初始化的 child 了,只初始化了 maxParams
                // 屬性,相當於是一個空的節點。insertChild 這個方法一會再去具體檢視,
                // 現在只要知道它在做具體的插入子節點的動作就行,先把這個方法追蹤完。
                n.insertChild(numParams, path, fullPath, handle)
                return

            // 如果公共字首和當前新增的路徑長度相等
            } else if i == len(path) { // Make node a (in-path) leaf
                // 如果當前節點不是 static 型別的, 就是已經註冊了 handle 的節點
                // 就證明已經註冊過這個路由,直接 panic 了
                if n.handle != nil {
                    panic("a handle is already registered for path '" + fullPath + "'")
                }

                // 如果是 nil 的,就是沒有註冊,證明這個節點是之前新增別的節點時候拆分出來的共同字首
                // 例如: 新增過兩個截點 /submit 和 /subject ,處理後就會有一個 static 的字首 sub
                // 這是再新增一個 /sub 的路由,就是這裡的情況了。

                n.handle = handle
            }

            // 這個新的節點被新增了, 出現了 return , 只有出現這個才會正常退出迴圈,一次新增完成。
            return
        }
    } else { // Empty tree
        // 如果 n 是一個空格的節點,就直接呼叫插入子節點方法
        n.insertChild(numParams, path, fullPath, handle)

        // 並且它只有第一次插入的時候才會是空的,所以將 nType 定義成 root
        n.nType = root
    }
}

incrementChildPrio

上面分析 addRoute 的時候呼叫了兩次這個方法,大概知道它是處理優先順序的,下面就追蹤到這個方法,看看它到底幹了什麼:

// increments priority of the given child and reorders if necessary
// 通過之前兩次的呼叫,我們知道,這個 pos 都是 n.indices 中指定字元的索引,也就是位置
func (n *node) incrementChildPrio(pos int) int {
    // 因為 children 和 indices 是同時新增的,所以索引是相同的
    // 可以通過 pos 代表的位置找到, 將對應的子節點的優先順序 + 1
    n.children[pos].priority++
    prio := n.children[pos].priority

    // 調整位置(向前移動)
    newPos := pos

    // 這裡的操作就是將 pos 位置的子節點移動到最前面,增加優先順序,比如:
    // 原本的節點是 [a, b, c, d], 傳入的是 2 ,就變成 [c, a, b, d]
    // 這只是個示例,實際情況還要考慮 n.children[newPos-1].priority < prio ,不一定是移動全部
    for newPos > 0 && n.children[newPos-1].priority < prio {
        // swap node positions
        n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]

        newPos--
    }

    // build new index char string
    // 重新組織 n.indices 這個索引字串,和上面做的是相同的事,只不過是用切割 slice 方式進行的
    // 從程式碼本身來說上面的部分也可以通過切割 slice 完成,不過移動應該是比切割組合 slice 效率高吧
    // 感覺是這樣,沒測試過,有興趣的可以測試下。
    if newPos != pos {
        n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
            n.indices[pos:pos+1] + // the index char we move
            n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
    }

    // 最後返回的是調整後的位置,保證外面呼叫這個方法之後通過索引找到的對應的子節點是正確的
    return newPos
}

程式碼看完,大概總結一下,這個方法主要是在處理節點之前的關係,新增或者修改已經存在的,拼接出樹的結構,真正寫入插入節點的資料是在這個方法處理完關係後,呼叫 insertChild 方法來完成的。

insertChild

現在就再追蹤下,看看它是怎麼插入資料的:

開啟之後看一下,又是很長…… 有沒有大腦翁的一下,仔細一行一行看吧。

// 它傳入的幾個引數,我們可以回到 addRoute 看看都傳給它什麼了
// numParams 引數個數
// path 插入的子節點的路徑
// fullPath 完整路徑,就是註冊路由時候的路徑,沒有被處理過的
// 註冊路由對應的 handle 函式
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
    var offset int // 已經處理過的路徑的所有位元組數

    // find prefix until first wildcard (beginning with ':'' or '*'')
    // 查詢字首,知道第一個萬用字元( 以 ':' 或 '*' 開頭
    // 就是要將 path 遍歷,提取出引數
    // 只要不是萬用字元開頭的就不做處理,證明這個路由是沒有引數的路由
    for i, max := 0, len(path); numParams > 0; i++ {
        c := path[i]

        // 如果不是 : 或 * 跳過本次迴圈,不做任何處理
        if c != ':' && c != '*' {
            continue
        }

        // 查詢萬用字元後面的字元,直到查到 '/' 或者結束
        end := i + 1

        for end < max && path[end] != '/' {
            switch path[end] {
            // 萬用字元後面的名稱不能包含 : 或 * , 如 ::name 或 :*name 不允許定義
            case ':', '*':
                panic("only one wildcard per path segment is allowed, has: '" +
                    path[i:] + "' in path '" + fullPath + "'")
            default:
                end++
            }
        }

        // 檢查萬用字元所在的位置,是否已經有子節點,如果有,就不能再插入
        // 例如: 已經定義了 /hello/name , 就不能再定義 /hello/:param
        if len(n.children) > 0 {
            panic("wildcard route '" + path[i:end] +
                "' conflicts with existing children in path '" + fullPath + "'")
        }

        // 檢查萬用字元是否有一個名字
        // 上面定義 end = i+1 , 後面的 for 又執行了 ++ 操作,所以萬用字元 : 或 * 後面最少
        // 要有一個字元, 如: :a 或 :name , :a 的時候 end 就是 i+2
        // 所以如果 end - i < 2 ,就是萬用字元後面沒有對應的名稱, 就會 panic
        if end-i < 2 {
            panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
        }

        // 如果 c 是 : 萬用字元的時候
        if c == ':' { // param
            // split path at the beginning of the wildcard
            // 從 offset 的位置,到查詢到萬用字元的位置分割 path
            // 並把分割出來的路徑定義到節點的 path 屬性
            if i > 0 {
                n.path = path[offset:i]
                // 開始的位置變成了萬用字元所在的位置
                offset = i
            }

            // 將引數部分定義成一個子節點
            child := &node{
                nType:     param,
                maxParams: numParams,
            }

            // 用新定義的子節點初始化一個 children 屬性
            n.children = []*node{child}
            // 標記上當前這個節點是一個包含引數的節點的節點
            n.wildChild = true

            // 將新建立的節點定義為當前節點,這個要想一下,到這裡這種操作已經有不少了
            // 因為一直都是指標操作,修改都是指標的引用,所以定義好的層級關係不會被改變
            n = child
            // 新的節點權重 +1
            n.priority++
            // 最大引數個數 - 1
            numParams--

            // if the path doesn't end with the wildcard, then there
            // will be another non-wildcard subpath starting with '/'
            // 這個 end 有可能是結束或者下一個 / 的位置
            // 如果小於路徑的最大長度,代表還包含子路徑(也就是說後面還有子節點)
            if end < max {
                // 將萬用字元提取出來,賦值給 n.path, 現在 n.path 是 :name 這種格式的字串
                n.path = path[offset:end]
                // 將起始位置移動到 end 的位置
                offset = end

                // 定義一個子節點,無論後面還有沒有子節點 :name 這種格式的路由後面至少還有一個 /
                // 因為引數型別的節點不會儲存 handler
                child := &node{
                    maxParams: numParams,
                    priority:  1,
                }

                // 將初始化的子節點賦值給當前節點
                n.children = []*node{child}

                // 當前節點又變成了新的子節點(到此時的 n 的身份已經轉變了幾次了,看這段代表的時候
                // 腦中要有一顆樹,實在想不出來的話可以按照開始的圖的結構,將節點的變化記錄到一張紙上,
                // 然後將每一次的轉變標記出來,就完全能明白了)
                n = child

                // 如果進入倒了這個 if ,執行到這裡,就還會進入到下一次迴圈,將可能生成出來的引數再次去匹配
            }

            // 如果走到了這裡,並且是迴圈的最後一次,就是已經將當前節點 n 定義成了葉子節點
            // 就可以進入到最下面程式碼部分,進行插入了
            // (上面的註釋說明的位置是 else 上面,不是為下面的 else 做的註釋)

        // 進入到 else ,就是包含 * 號的路由了 
        } else { // catchAll
            // 這裡的意思是, * 匹配的路徑只允許定義在路由的最後一部分 
            // 比如 : /hello/*world 是允許的, /hello/*world/more 這種就會 painc
            // 這種路徑就是會將 hello/ 後面的所有內容變成 world 的變數
            // 比如位址列輸入: /hello/one/two/more ,獲取到的引數 world = one/twq/more
            // 不會再將後面的 / 作為路徑處理了
            if end != max || numParams > 1 {
                panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
            }

            // 這種情況是,新定義的 * 萬用字元路由和其他已經定義的路由衝突了
            // 例如已經定義了一個 /hello/bro , 又定義了一個 /hello/*world ,此時就會 panic 了
            if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
                panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
            }

            // currently fixed width 1 for '/'
            i--
            // 這裡是查詢萬用字元前面是否有 / 沒有 / 是不行的,panic
            if path[i] != '/' {
                panic("no / before catch-all in path '" + fullPath + "'")
            }

            n.path = path[offset:i]

            // first node: catchAll node with empty path
            // 後面的套路基本和之前看到的類似,就是定義一個子節點,儲存萬用字元前面的路徑,
            // 有變化的就是將 nType 定義為 catchAll,就是說代表這是一個  * 號匹配的路由
            child := &node{
                wildChild: true,
                nType:     catchAll,
                maxParams: 1,
            }
            n.children = []*node{child}
            n.indices = string(path[i])
            n = child
            n.priority++

            // second node: node holding the variable
            // 將下面的節點再新增到上面,不過 * 號路由不會再有下一級的節點了,因為它會將後面的
            // 的所有內容當做變數,即使它是個 / 符號
            child = &node{
                path:      path[i:],
                nType:     catchAll,
                maxParams: 1,
                handle:    handle,
                priority:  1,
            }
            n.children = []*node{child}

            // 這裡 return 了,看下上面,因為已經將 handle 儲存,查到了葉子節點,所以就直接結束了當前方法
            return
        }
    }

    // insert remaining path part and handle to the leaf
    // 這裡給所有的處理的完成後的節點(不包含 * 萬用字元方式)的 handle 和 path 賦值
    n.path = path[offset:]
    n.handle = handle
}

程式碼讀完,簡單分析下,就是將沒有了上級管理的路由(關係被 addRoute 方法處理了), 交給這個方法處理,處理的時候會根據實際情況搞出新的關係,並最終在葉子節點寫入註冊的資料(主要是 path 和 handler)

getValue

前面已經分析完成怎麼向樹中插入資料,現在不知道是否還記得,在 ServeHTTP 方法中還用到了個從樹中取資料的方法 getValue, 其實插入如果搞明白了,這個取的方法理解起來就很容易了,按照插入時定義的規則,取出來即可,直接看程式碼:

// 通過傳入的引數的路徑,尋找指定的路由
// 引數: 字串型別的路徑
// 返回值:
// Handle 通過路徑找到的 handler , 如果沒有找到,會返回一個 nil
// Params 如果路由註冊的是引數路由,這裡會將引數及值返回,是一個 Param 結構的 slice 
// tsr 是一個 boolean 型別的標誌,告訴呼叫者這個路由是否可以被重定向,配合 Router
// 裡面的 RedirectTrailingSlash 屬性
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
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):]
                // 如果當前的節點不包含萬用字元(子節點有 : 或者 * 這種的),就可以直接去子節點繼續搜尋。
                if !n.wildChild {
                    // 通過查詢路徑的首字元,快速找到包含這個字元的子節點(效能提升的地方就體現出來了)
                    c := path[0]
                    for i := 0; i < len(n.indices); i++ {
                        // 如果找到了,就繼續去 indices 中對應的子節點去找
                        // 並且進入下一次迴圈
                        if c == n.indices[i] {
                            n = n.children[i]
                            continue walk
                        }
                    }

                    // 如果進入到了這裡,代表沒有找到對應的子節點

                    // 如果尋找路徑正好是 / ,因為去除了公共路徑
                    // 假設尋找的是 /hello/ , n.path = hello, 這時 path 就是 /
                    // 當前的節點如果註冊了 handle ,就證明這個是一個路由,tsr 就會變成 true
                    // 呼叫者就知道可以將路由重定向到 /hello 了,下次再來就可以找到這個路由了
                    // 可以去 Router.ServeHTTP 方法看下,是不是會發起一個重定向
                    tsr = (path == "/" && n.handle != nil)

                    // 返回,此時會返回 (nil,[],true|false) 這 3 個值
                    return
                }

                // 處理是萬用字元節點(包含引數或*)的情況
                // 如果當前節點是萬用字元節點,子節點肯定只有一個,因為 :name 這種格式的只允許註冊一個
                // 所以可以把當前節點替換成 :name 或 *name 這種子節點
                n = n.children[0]
                switch n.nType {
                // 當子節點是引數型別的時候,:name 這種格式的
                case param:
                    // find param end (either '/' or path end)
                    // 查詢出尋找路徑中到結束或者 / 以前的部分所在的位置
                    // 比如是 :name 或者 :name/more ,end 就會等於 4
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }

                    // save param value
                    // 儲存引數的值, 因為引數是一個 slice ,使用前要初始化一下
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                        // 現在 p 的值是 []Param
                    }
                    // 下面兩行是將 slice 的長度擴充套件
                    // 如果第一次來, p 是一個空格, i = 0
                    // p = p[:0+1] p 取的是一個位置 0 到 1 的切片,因為切片前開後閉,
                    // 所以它的長度就變成了 1, 下面就可以向切片新增一個值了
                    // 第二次來就是在原有的長度再擴充套件 1 的長度,然後再新增一個值,
                    // 這樣這個引數就會永遠是一個有效的最小長度,應該可以提高效能 ?
                    // 不清楚作者為什麼這樣做,直接 append 不可以嗎? 有興趣的可以去確定下。
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity

                    // 這個就是給 Parm 賦值, Key 就是去掉 : 後的值,比如 :name 就是 name
                    // Value 就是路徑到結束位置 end 的值
                    p[i].Key = n.path[1:]
                    p[i].Value = path[:end]

                    // we need to go deeper!
                    // 上面已經獲取了第一個值,但是有可能還會有兩個或者更多,所以還要繼續處理
                    // 如果之前找到第一個值的位置比尋找路徑的長度小,就是還有 :name 後面還有內容需要去匹配
                    if end < len(path) {
                        // 如果還存在子節點, 就將 path 和 node 替換成子節點的,跳出當前迴圈再找一遍
                        if len(n.children) > 0 {
                            path = path[end:]
                            n = n.children[0]
                            continue walk
                        }

                        // ... but we can't
                        tsr = (len(path) == end+1)
                        // 這裡返回的是 (nil, [Parm{name, 路徑上的值}], true|false)
                        return
                    }

                    // 如果當前節點儲存了 handle ,就將 handle 返回給呼叫者,去發起業務邏輯
                    if handle = n.handle; handle != nil {
                        return
                    } else if len(n.children) == 1 {
                        // No handle found. Check if a handle for this path + a
                        // trailing slash exists for TSR recommendation
                        // 確認它是否有一個 / 的子節點,如果有 tsr = true
                        n = n.children[0]
                        tsr = (n.path == "/" && n.handle != nil)
                    }

                    // 這裡返回的是 [nil, [找到的引數...], true|false]
                    return

                // 如果是 * 這種方式的路由, 就是直接處理引數,並返回,因為它肯定是最後一個節點
                // 所以也不會出現子節點,及時路徑上後面根再多的 / 也都只是引數的值
                case catchAll:
                    // save param value
                    if p == nil {
                        // lazy allocation
                        p = make(Params, 0, n.maxParams)
                    }
                    i := len(p)
                    p = p[:i+1] // expand slice within preallocated capacity
                    p[i].Key = n.path[2:]
                    p[i].Value = path

                    handle = n.handle
                    return

                default:
                    // 沒想到什麼情況會出現無效的型別, 如果出現了就直接 panic
                    // 現在這個 panic 不會導致伺服器崩潰,因為在 ServeHTTP 中做了 Recovery
                    panic("invalid node type")
                }
            }
        // 如果查詢的路徑和節點儲存的路徑相同
        } else if path == n.path {
            // We should have reached the node containing the handle.
            // Check if this node has a handle registered.
            // 檢查下這個節點是否包含 handle ,如果包含就返回
            // 會有不包含的情況,比如恰巧這個就是一個相同字首的節點
            if handle = n.handle; handle != nil {
                return
            }

            // 如果查詢的路徑是 / 又不是根節點,還是個引數節點,就允許重定向
            if path == "/" && n.wildChild && n.nType != root {
                tsr = true
                return
            }

            // No handle found. Check if a handle for this path + a
            // trailing slash exists for trailing slash recommendation
            // 如果沒有找到,但是有一個 / 的子節點,也允許它重定向
            // 就是這個意思,比如定義了一個 /hello/ 路由,但是訪問的是 /hello
            // 也允許它重定向到 /hello/ 上
            for i := 0; i < len(n.indices); i++ {
                if n.indices[i] == '/' {
                    n = n.children[i]
                    tsr = (len(n.path) == 1 && n.handle != nil) ||
                        (n.nType == catchAll && n.children[0].handle != nil)
                    return
                }
            }

            return
        }

        // Nothing found. We can recommend to redirect to the same URL with an
        // extra trailing slash if a leaf exists for that path
        // 是否允許重定向的一個驗證
        tsr = (path == "/") ||
            (len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
                path == n.path[:len(n.path)-1] && n.handle != nil)
        return
    }
}

到這裡原始碼就分析完了,整理的程式碼量不算多, router.go 檔案還好理解,基本看一遍就清除,這個 tree.go ,看了幾個小時才基本上看明白,邏輯有點複雜,而且也不是很習慣這個寫法。 搞明白了之後再用的時候遇到問題也是很清楚哪裡出現的,並且定義路由的時候也知道要怎麼去定義了,那種可以支援,那種不可以支援。

相關文章