47

在Golang的HTTP请求中共享数据

 5 years ago
source link: https://huoding.com/2019/02/08/718?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

首先,我们需要先明确一下问题的描述:本文所要讨论的共享数据可不是指的 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,结果依次是:

  1. request_id: uuid
  2. 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)
}

本文是仓促写于返京的途中,未做严格验证,如有谬误敬请海涵。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK