Go 部落格
上下文和結構體
引言
在許多 Go API 中,尤其是現代 API 中,函式和方法的第一個引數通常是 context.Context
。Context 提供了一種跨 API 邊界和程序傳輸截止時間、呼叫者取消以及其他請求範圍值的方法。當庫直接或間接與遠端伺服器(如資料庫、API 等)互動時,通常會使用它。
有關 context 的文件指出:
Context 不應儲存在 struct 型別中,而應傳遞給每個需要它的函式。
本文將透過原因和示例來擴充套件此建議,描述為什麼將 Context 作為引數傳遞而不是將其儲存在其他型別中很重要。它還強調了一個罕見的、儲存 Context 在 struct 型別中可能是有意義的情況,以及如何安全地進行。
優先使用作為引數傳遞的上下文
為了理解不將 context 儲存在 struct 中的建議,讓我們考慮首選的 context-as-argument 方法。
// Worker fetches and adds works to a remote work orchestration server.
type Worker struct { /* … */ }
type Work struct { /* … */ }
func New() *Worker {
return &Worker{}
}
func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
_ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}
func (w *Worker) Process(ctx context.Context, work *Work) error {
_ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}
在這裡,(*Worker).Fetch
和 (*Worker).Process
方法都直接接受一個 context。透過這種按引數傳遞的設計,使用者可以設定每個呼叫的截止時間、取消和元資料。而且,很清楚傳遞給每個方法的 context.Context
將如何使用:不期望傳遞給一個方法的 context.Context
會被其他任何方法使用。這是因為 context 的範圍被限制在它需要的大小,這大大增加了此包中 context
的實用性和清晰度。
將 context 儲存在 struct 中會導致混淆
讓我們再次檢查上面帶有不推薦的 context-in-struct 方法的 Worker
示例。問題在於,當您將 context 儲存在 struct 中時,您會向呼叫者隱藏生命週期,或者更糟的是,以不可預測的方式混合兩個範圍。
type Worker struct {
ctx context.Context
}
func New(ctx context.Context) *Worker {
return &Worker{ctx: ctx}
}
func (w *Worker) Fetch() (*Work, error) {
_ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}
func (w *Worker) Process(work *Work) error {
_ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}
(*Worker).Fetch
和 (*Worker).Process
方法都使用儲存在 Worker 中的 context。這阻止了 Fetch 和 Process 的呼叫者(他們自己可能有不同的 context)在每次呼叫時指定截止時間、請求取消和附加元資料。例如:使用者無法僅為 (*Worker).Fetch
提供截止時間,或僅取消 (*Worker).Process
呼叫。呼叫者的生命週期與共享 context 交織在一起,而 context 的範圍被限制在建立 Worker
的生命週期內。
與按引數傳遞的方法相比,API 對使用者來說也更加混亂。使用者可能會問自己:
- 既然
New
接受context.Context
,那麼建構函式是否正在執行需要取消或截止時間的工作? - 傳遞給
New
的context.Context
是否適用於(*Worker).Fetch
和(*Worker).Process
中的工作?都不適用?一個適用,另一個不適用?
API 需要大量的文件來明確告知使用者 context.Context
的確切用途。使用者可能還需要閱讀程式碼,而不是依賴 API 的結構來傳達資訊。
最後,設計一個生產級的伺服器,其請求沒有各自的 context,因此無法充分響應取消,這是非常危險的。如果沒有能力設定每個呼叫的截止時間,您的程序可能會積壓並耗盡其資源(如記憶體)!
例外情況:保持向後相容性
當 Go 1.7 — 引入了 context.Context — 釋出時,大量 API 必須以向後相容的方式新增 context 支援。例如,net/http
的 Client
方法,如 Get
和 Do
,非常適合 context。使用這些方法傳送的每個外部請求都將受益於 context.Context
帶來的截止時間、取消和元資料支援。
有兩種方法可以以向後相容的方式新增對 context.Context
的支援:將 context 包含在 struct 中,如我們稍後將看到的,以及複製函式,其中複製的函式接受 context.Context
並在函式名字尾上新增 Context
。應優先使用複製方法而不是 context-in-struct 方法,並且在使模組保持相容中有更詳細的討論。然而,在某些情況下,這是不切實際的:例如,如果您的 API 暴露了大量函式,那麼複製所有函式可能是不可行的。
net/http
包選擇了 context-in-struct 方法,這提供了一個有用的案例研究。讓我們看看 net/http
的 Do
。在引入 context.Context
之前,Do
定義如下:
// Do sends an HTTP request and returns an HTTP response [...]
func (c *Client) Do(req *Request) (*Response, error)
Go 1.7 之後,如果不是為了破壞向後相容性,Do
可能看起來像下面這樣:
// Do sends an HTTP request and returns an HTTP response [...]
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
但是,保持向後相容性和遵守Go 1 向後相容性承諾對於標準庫至關重要。因此,維護者選擇將 context.Context
新增到 http.Request
struct 中,以便在不破壞向後相容性的情況下支援 context.Context
。
// A Request represents an HTTP request received by a server or to be sent by a client.
// ...
type Request struct {
ctx context.Context
// ...
}
// NewRequestWithContext returns a new Request given a method, URL, and optional
// body.
// [...]
// The given ctx is used for the lifetime of the Request.
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// Simplified for brevity of this article.
return &Request{
ctx: ctx,
// ...
}
}
// Do sends an HTTP request and returns an HTTP response [...]
func (c *Client) Do(req *Request) (*Response, error)
在改造您的 API 以支援 context 時,將 context.Context
新增到 struct 中可能是合理的,如上所示。但是,請記住首先考慮複製您的函式,這可以以向後相容的方式改造 context.Context
,而不會犧牲實用性和理解性。例如:
// Call uses context.Background internally; to specify the context, use
// CallContext.
func (c *Client) Call() error {
return c.CallContext(context.Background())
}
func (c *Client) CallContext(ctx context.Context) error {
// ...
}
結論
Context 可以輕鬆地將重要的跨庫和跨 API 資訊向下傳播到呼叫堆疊。但是,為了保持可理解性、易於除錯和有效性,必須一致且清晰地使用它。
當作為方法的第一個引數傳遞而不是儲存在 struct 型別中時,使用者可以充分利用其可擴充套件性,透過呼叫堆疊構建一個強大的取消、截止時間和元資料資訊樹。而且,最重要的是,當它作為引數傳遞時,其範圍清晰可懂,從而在呼叫堆疊的上下文中實現了清晰的理解和除錯能力。
在設計帶有 context 的 API 時,請記住這個建議:將 context.Context
作為引數傳遞;不要將其儲存在 struct 中。
延伸閱讀
下一篇文章:Go 開發者調查 2020 年結果
上一篇文章:Go 1.16 中的新模組更改
部落格索引