正小歪 BLOG

「译」Go Context 在 HTTP 传播

Go 1.7 引入了一个内置的 context 类型,在系统中可以使用 Context 来传递元数据,例如不同函数或者不同线程甚至进程的传递 Request ID。

Go 将 Context 包引入标准库以统一 context 的使用。在此之前每个框架或者库都有自己的 context 。它们之间还无法兼容,导致了碎片化,最终在各处 context 的传播上就有不少的麻烦。

虽然在同一个处理过程中有一个通用的 context 传播机制是非常有用的,但是 Go 的 Context 包并没有提供该功能。就像上面描述的,context 会在网络中被不同的处理过程传递。例如在多服务架构中,一个请求往往会在多个地方被处理 (多个微服务,消息队列,数据库等),直到最后响应给用户。能够在多个处理过程中传递 context 显得尤为重要。

如果你要在 HTTP 中传播 context ,需要你对 context 进行序列化处理。类似的,在接收端也要解析,同时把值放入当前的 context 中。假设我们希望在 context 中传递 request ID。

package request
import "context"

// WithID 把 request ID 放入当前的 context 中
func WithID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, contextIDKey, id)
}

// IDFromContext 返回从 context 中获取的 request ID
// 如果 context 中没有定义就返回空值
func IDFromContext(ctx context.Context) string {
	v := ctx.Value(contextIDKey)
    if v == nil {
    		return ""
    }
	return v.(string)
}

type contextIDType struct{}

var contextIDKey = &contextIDType{}

// ...

WithID 允许我们把 request ID 设置到 context 中,IDFromContext 可以从 context 中读取 request ID。一旦我们有在多个处理过程,就需要手动把到 context 设置到传输中,同时在接受端解析然后写入 context。

在 HTTP 中我们可以从 header 中获取 request ID。大多数的 context 都可以通过 header 来传播。一些传输层可能不支持 headers 或者 headers 不是传输标准 (例如有大小限制或者缺少加密措施)。在这种情况下,由具体实现来决定如何传递上下文。

HTTP 传播

目前没有直接的方法可以在 HTTP reuqest 中的值放入 context 中。由于无法遍历出 context 的值,因此也无法一次性转换整个上下文。

const requestIDHeader = "request-id"

// Transport 把 request context 序列化到 request headers
type Transport struct {
    // Base 是构建请求的真实 round tripper
    // 如果没有被设置,默认使用 http.DefaultTransport
	Base http.RoundTripper
}


// RoundTrip 转换 request context 到 headers 中
// 同时构建请求
func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
	r = cloneReq(r) // per RoundTrip interface enforces
    
	rid := request.IDFromContext(r.Context())
    if rid != "" {
    	r.Header.Add(requestIDHeader, rid)
    }
    
    base := t.Base
    if base == nil {
    	base = http.DefaultTransport
    }
    return base.RoundTrip(r)
}

在上面的 Transport 中,如果 request ID 存在就会被当做 “request-id” header 进行传递。

类似的方法可以解析请求,把 “request-id” 放入请求的上下文中。

// Handler 从 request headers 反序列化到 request context 中
type Handler struct {
    // Base 是完成反序列化调用的真实方法
    Base http.Handler
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    rid := r.Header.Get(requestIDHeader)
    if rid != "" {
        r = r.WithContext(request.WithID(r.Context(), rid))
    }
    h.Base.ServeHTTP(w, r)
}

为了继续传播 context ,请确保在你的方法中把当前的 context 传递到下一个 request 。传入的 context 将会随着 request 传播到 https://endpoint。

http.Handle("/", &Handler{
    Base: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        req, _ := http.NewRequest("GET", "https://endpoint", nil)
        // 传播当前的 context
        req = req.WithContext(r.Context()) 
        // Make the request.
    }),
})