Go 部落格
背景與結構
簡介
在許多 Go API 中,尤其是現代 API,函式和方法的第一個引數常是 context.Context
。背景提供了在 API 邊界和處理程序之間傳遞截止時間、呼叫方取消及其他請求範圍值的方法。在函式庫直接或間接與遠端伺服器(例如資料庫、API 等)互動時,通常會使用背景。
背景文件指出
不可將背景儲存在結構類型中,而應傳遞給每個需要的函式。
本文進一步說明該建議的理由和範例,說明傳遞背景而非將其儲存在其他類型中很重要的原因。它也重點說明在結構類型中儲存背景的罕見案例可能有意義,以及如何安全地執行此操作。
優先傳遞背景作為引數
要了解不將背景儲存在結構中的建議,讓我們來考慮優先選擇以背景為引數的方式
// 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
的效用和明確程度。
將內容儲存在結構會造成混亂
讓我們再次檢查上面範例中的 Worker
,使用不受歡迎的將內容儲存在結構內的方式。其問題在於當你將內容儲存在結構時,會對呼叫者隱藏生命週期,或更糟的是,會以無法預測的方式將兩個範圍混在一起。
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 內的內容。這會阻礙 Fetch 和 Process 的呼叫者(本身可能具有不同的內容)指定最後期限、要求取消,以及針對每個呼叫附加資料。例如:使用者無法僅對 (*Worker).Fetch
提供最後期限,或僅取消 (*Worker).Process
呼叫。呼叫者的生命週期會與共用內容混在一起,而且內容的範圍會限制在建立 Worker
的生命週期內。
與傳遞引數的方式相較之下,此 API 對使用者而言也會混淆許多。使用者可能會問自己
- 由於
New
會接收context.Context
,建構函式是否會執行需要取消或最後期限的工作? - 傳遞給
New
的context.Context
是否會套用於(*Worker).Fetch
和(*Worker).Process
內的工作?兩者皆否?是其一,但不是另一者?
此 API 需要大量文件來明確告知使用者 context.Context
的確切用途。使用者可能也必須閱讀程式碼,而不是能依賴 API 的結構傳達的內容。
最後,設計一個每個要求都沒有內容的生產等級伺服器可能很危險,因為這樣一來無法適當尊重新取消。若無法設定每個呼叫的最後期限,你的程序可能會造成積壓,而且會耗盡其資源(例如記憶體)!
規則的例外:維護後向相容性
當推出 Go 1.7 (導入 context.Context) 時,數量龐大的 API 必須以回溯相容的方式加入對 context 的支援。例如,net/http
的 Client
方法(如 Get
和 Do
)是 context 的最佳候選者。傳送至這些方法的每個外部要求都會受益於 context.Context
附帶的截止時間、取消以及元資料支援。
有兩種方法可以以回溯相容的方式加入對 context.Context
的支援:在結構中納入 context(稍後會說明)以及複製函式(其中,重複的函式會接受 context.Context
,並將 Context
作為它們的函式名稱字尾)。複製法應該是優先考量,而不是結構中的 context,且在 維持模組相容性 中進一步加以討論。然而,在某些情況下,這並不可行:例如,如果您的 API 揭露大量的函式,那麼複製它們會不可行。
net/http 套件選擇結構中的 context 方法,提供了有用的個案研究。我們來看看
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 對標準程式庫至關重要。所以,維護者選擇在 http.Request
結構中加入 context.Context
,以允許支援 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
可能有道理,如同上方。但是,請記得先考慮複製函式,這允許在不犧牲實用性和理解能力的情況下,以回溯相容方式改造 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 資訊變得容易。但是,必須持續且清楚地使用,才能保持其易於理解、除錯與有效性。
將其傳遞為方法中的第一個參數,而不是儲存在結構類型中時,使用者可以充分利用可擴充性,透過呼叫堆疊建立強大的取消、時間限制以及元資料資訊樹狀圖。最棒的是,將其傳遞為參數時,其範圍能夠清楚地被理解,因而能清楚理解和除錯堆疊上下文。
在設計帶有 context 的 API 時,請記住這個建議:將 context.Context
傳遞為參數,不要儲存在結構中。