「譯」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. }), })