Go語言router請求路由

2020-07-16 10:05:05
在常見的 Web 框架中,router 是必備的元件。Go語言圈子裡 router 也時常被稱為 http 的 multiplexer。通過前面幾節的學習,我們已經知道了如何用 http 標準庫中內建的 mux 來完成簡單的路由功能了。如果開發 Web 系統對路徑中帶引數沒什麼興趣的話,用 http 標準庫中的 mux 就可以。

RESTful 是幾年前颳起的 API 設計風潮,在 RESTful 中除了 GET 和 POST 之外,還使用了 HTTP 協定定義的幾種其它的標準化語意。具體包括:

const (
    MethodGet = "GET"
    MethodHead = "HEAD"
    MethodPost = "POST"
    MethodPut = "PUT"
    MethodPatch = "PATCH" // RFC 5789
    MethodDelete = "DELETE"
    MethodConnect = "CONNECT"
    MethodOptions = "OPTIONS"
    MethodTrace = "TRACE"
)

來看看 RESTful 中常見的請求路徑:

GET /repos/:owner/:repo/comments/:id/reactions
POST /projects/:project_id/columns
PUT /user/starred/:owner/:repo
DELETE /user/starred/:owner/:repo

相信聰明的你已經猜出來了,這是 Github 官方文件中挑出來的幾個 API 設計。RESTful 風格的 API 重度依賴請求路徑。會將很多引數放在請求 URL 中。除此之外還會使用很多並不那麼常見的 HTTP 狀態碼,不過本節只討論路由,所以先略過不談。

如果我們的系統也想要這樣的 URL 設計,使用標準庫的 mux 顯然就力不從心了。

httprouter

較流行的開源 go Web 框架大多使用 httprouter,或是基於 httprouter 的變種對路由進行支援。前面提到的 github 的引數式路由在 httprouter 中都是可以支援的。

因為 httprouter 中使用的是顯式匹配,所以在設計路由的時候需要規避一些會導致路由衝突的情況,例如:

conflict:
GET /user/info/:name
GET /user/:id
no conflict:
GET /user/info/:name
POST /user/:id

簡單來講的話,如果兩個路由擁有一致的 http 方法(指 GET/POST/PUT/DELETE)和請求路徑字首,且在某個位置出現了 A 路由是 wildcard(指 :id 這種形式)引數,B 路由則是普通字串,那麼就會發生路由衝突。路由衝突會在初始化階段直接 panic:

panic: wildcard route ':id' conflicts with existing children in
path '/user/:id'
goroutine 1 [running]:
github.com/cch123/httprouter.(*node).insertChild(0xc4200801e0, 0xc42004fc01, 0x126b177, 0x3, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:256 +0x841
github.com/cch123/httprouter.(*node).addRoute(0xc4200801e0, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:221 +0x22a
github.com/cch123/httprouter.(*Router).Handle(0xc42004ff38, 0x126a39b, 0x3, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:262 +0xc3
github.com/cch123/httprouter.(*Router).GET(0xc42004ff38, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:193 +0x5e
main.main()
/Users/caochunhui/test/go_web/httprouter_learn2.go:18 +0xaf
exit status 2

還有一點需要注意,因為 httprouter 考慮到字典樹的深度,在初始化時會對引數的數量進行限制,所以在路由中的引數數目不能超過 255,否則會導致 httprouter 無法識別後續的引數。不過這一點上也不用考慮太多,畢竟 URL 是人設計且給人來看的,相信沒有長得誇張的 URL 能在一條路徑中帶有 200 個以上的引數。

除支援路徑中的 wildcard 引數之外,httprouter 還可以支援 * 號來進行通配,不過 * 號開頭的引數只能放在路由的結尾,例如下面這樣:

Pattern: /src/*filepath

/src/                                 filepath = ""
/src/somefile.go              filepath = "somefile.go"
/src/subdir/somefile.go   filepath = "subdir/somefile.go"

這種設計在 RESTful 中可能不太常見,主要是為了能夠使用 httprouter 來做簡單的 HTTP 靜態檔案伺服器。

除了正常情況下的路由支援,httprouter 也支援對一些特殊情況下的回撥函數進行客製化,例如 404 的時候:

r := httprouter.New()
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("oh no, not found"))
})

或者內部 panic 的時候:

r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
    log.Printf("Recovering from panic, Reason: %#v", c.(error))
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(c.(error).Error()))
}

目前開源界最為流行的 Web 框架 gin 使用的就是 httprouter 的變種。

原理

httprouter 和眾多衍生 router 使用的資料結構被稱為壓縮字典樹(Radix Tree)。大家可能沒有接觸過壓縮字典樹,但對字典樹(Trie Tree)應該有所耳聞。下圖是一個典型的字典樹結構:

字典樹
圖:字典樹