關於 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
檔案貌似都已經追蹤完了,還整下兩個函式沒有提到: 和 Lookup
和 ServeFiles
。
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
,看了幾個小時才基本上看明白,邏輯有點複雜,而且也不是很習慣這個寫法。 搞明白了之後再用的時候遇到問題也是很清楚哪裡出現的,並且定義路由的時候也知道要怎麼去定義了,那種可以支援,那種不可以支援。