首先,我们需要先明确一下问题的描述:本文所要讨论的共享数据可不是指的 cookie、session 之类的概念,它们描述的是在「请求间」共享数据,而我们关注的是在「请求中」共享数据,也就说是,在每个请求中的各个 middleware 和 handler 之间共享数据。
实际上,我之所以关注这个问题是因为 httprouter,众所周知,httprouter 是目前 Golang 社区最流行的 HTTP 路由库,不过它有一个问题,其 handler 参数定义如下:
func (http.ResponseWriter, *http.Request, httprouter.Params)
而 Golang 官方的 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(), "id", "uuid") 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("id") 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{},也就是说,你可以使用任意值作为键和值,相对应的,当你使用数据的时候,需要做对应的类型转换,从 interface{} 转成你需要的类型。
需要着重说明的一点是,最好不要使用基础类型来做 key,而应该使用自定义类型,就好像本例中的 RequestContextKey 类型,为什么要这样做?假设大家都是用 string 之类的基础类型来做 key 的话,那么我们就不容易区分这个 key 到底隶属于谁,代码里列出了键为基础类型(string)和自定义类型(RequestContextKey)两种调用方式,大家可以混合使用两种调用方式试试,就会发现有了 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) }
如此一来,即保证了兼容 http.Handler,又可以享受到 httprouter 的高性能。有兴趣的读者可以继续阅读:Build You Own Web Framework In Go。