在Golang的HTTP請求中共享資料
首先,我們需要先明確一下問題的描述:本文所要討論的共享資料可不是指的 cookie、session 之類的概念,它們描述的是在「請求間」共享資料,而我們關注的是在「請求中」共享資料,也就說是,在每個請求中的各個 middleware 和 handler 之間共享資料。
實際上,我之所以關注這個問題源自httprouter ,眾所周知,httprouter 是目前 Golang 社群最流行的 HTTP 路由庫,不過它有一個問題,其 handler 引數定義如下:
func (http.ResponseWriter, *http.Request, httprouter.Params)
而官方的 http.Handler 引數定義是:
func (http.ResponseWriter, *http.Request)
也就是說, httprouter 多了一個 httprouter.Params 引數,用來傳遞路由引數,可惜它破壞了相容性,關於此問題,官方給出了說明 :
The router itself implements the http.Handler interface. Moreover the router provides convenient adapters for http.Handlers and http.HandlerFuncs which allows them to be used as a httprouter.Handle when registering a route. The only disadvantage is, that no parameter values can be retrieved when a http.Handler or http.HandlerFunc is used, since there is no efficient way to pass the values with the existing function parameters. Therefore httprouter.Handle has a third function parameter.
大概意思是 httprouter 提供了相容模式,不過相容模式不能使用路由引數。那麼能不能在保持相容性的前提下使用路由引數呢,官方有過討論 ,計劃在新版本中使用 Context 來傳遞路由引數,但是幾年過去了,還沒實現。
讓我們先順著 Context 來看看如何在 Golang 的 HTTP 請求中共享資料。
路由的例子有點複雜,我們不妨假設一個簡單點兒的例子:設想一下我們需要給每一個請求分配一個請求 ID,並且每個 middleware 或者 handler 都可以拿到此請求 ID。很明顯,這個請求 ID 就是我們說的共享資料,下面讓我們看看如何用 Context 來實現它:
package main import ( "context" "fmt" "net/http" ) // RequestContextKey is a context key type RequestContextKey string func requestID(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), RequestContextKey("id"), "uuid") next(w, r.WithContext(ctx)) } } func test1(w http.ResponseWriter, r *http.Request) { id := r.Context().Value(RequestContextKey("id")) w.Write([]byte("request_id: " + id.(string))) } func test2(w http.ResponseWriter, r *http.Request) { id := fmt.Sprintf("%v", r.Context().Value(RequestContextKey("id"))) w.Write([]byte("request_id: " + id)) } func main() { http.Handle("/test1", requestID(test1)) http.HandleFunc("/test2", test2) http.ListenAndServe(":8080", nil) }
本例只用到了兩個 Context 方法,分別是:
- WithValue(parent Context, key, val interface{}) Context
- Value(key interface{}) interface{}
如上可見,key 和 val 都是 interface{},也就是說,你可以使用任意值作為鍵和值,與此對應的,當你取回值得時候,同樣需要做對應的型別轉換。
需要著重說明的一點是,最好不要使用基礎型別來做 key,而應該使用自定義型別,就好像本例中的 RequestContextKey 型別,為什麼要這樣做?假設大家都是用 string 之類的基礎型別來做 key 的話,那麼我們就不容易區分這個 key 到底隸屬於誰,很容易出現彼此影響的情況,Context 在讀寫資料的時候會保證型別安全,不會發生錯亂的情況。
明白了這些就可以執行程式碼了,先請求 /test1,再請求 /test2,結果依次是:
- request_id: uuid
- request_id: <nil>
也就是說,我們實現了在 HTTP 請求中共享資料的功能,同時可知 Context 的作用範圍是請求級的,不同請求的 Context 不會彼此干擾。
讓我們把目光回到文章開頭提到的 httprouter 身上,雖然它本身 和 http.Handler 有不相容的問題,但是我們可以通過前面學到的 Context 相關知識來改善此問題:
package main import ( "context" "net/http" "github.com/julienschmidt/httprouter" ) // RouterContextKey is a context key type RouterContextKey string func compatible(next http.HandlerFunc) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { ctx := context.WithValue(r.Context(), RouterContextKey("params"), p) next(w, r.WithContext(ctx)) } } func user(w http.ResponseWriter, r *http.Request) { p := r.Context().Value(RouterContextKey("params")).(httprouter.Params) w.Write([]byte(p.ByName("name"))) } func main() { router := httprouter.New() router.GET("/user/:name", compatible(user)) http.ListenAndServe(":8080", router) }
本文是倉促寫於返京的途中,未做嚴格驗證,如有謬誤敬請海涵。