官發(fā)博文:進一步闡明關于 context 的最佳實踐
關于 context 的一點最佳實踐

2021 年 2月 24 日,官方 blog 中詳細講了關于 context 使用的一些最佳實踐,提供了代碼示例,告訴你為何 context不應存儲在 struct 內部,最好的方式是作為函數(shù)的第一個參數(shù)傳遞,以及如何在非常必要的情況下(保持向后兼容)以一種最安全的方式將 context 存儲到 struct 中。下面是原文的主要內容。
Introduction
在許多 Go API,尤其是現(xiàn)代 API 中,函數(shù)和方法的第一個參數(shù)通常是 context.Context。context 提供了很多方法例如 WithCancel、WithDeadline、WithValue、以實現(xiàn)跨 API 的流程控制。很多 lib 在與遠程服務器(如數(shù)據(jù)庫、api等)交互時,經常使用 context 做控制。
context 的文檔中講到:
context 不應存儲在 struct 內部,最好的方式是作為函數(shù)的第一個參數(shù)傳遞
本文在此建議的基礎上講解了原因和示例,說明了為什么要使用函數(shù)傳遞 Context 而不是將其存儲在 struct 中的重要性。還以 net/http 的代碼為例,解釋了應該在何種情況下可以在 struct 中 存儲 Context 。
推薦 context 作為函數(shù)參數(shù)傳遞
讓我們先看下在函數(shù)中傳遞 context:
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, w *Work) error {
_ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}
這里 (*Worker).Fetch 和 (*Worker).Process 都直接將 context 作為函數(shù)第一個參數(shù)。這樣從 context 的生成到結束,調用方可以很清晰地知道 context 的傳遞路線。
將 context 存儲到 strcut 所帶來的一些困惑
在結構體中嵌套 context 來實現(xiàn)上面的 Worker 示例,調用者對所使用的 context 生命周期產生迷惑:
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(w *Work) error {
_ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}
(*Worker).Fetch 和 (*Worker).Process 方法同時使用了 Worker 結構體中的 context ,這種情況下使得調用方無法定義不同的 context ,比如有調用方想用 WitchCancel,有的想用 WithDeadline,也很難理解上面?zhèn)鱽淼?context 的作用是 cancel?還是 deadline?調用者所使用 context 的生命周期被綁定到了一個共享的 context 上面。
特殊情況:保留向后兼容性
當 go 1.7 版本發(fā)布時,大量的的 API 需要以向后兼容的方式支持 context.Context,例如,net/http 的 Client 方法(例如Get和Do)是使用 context 的典范。使用這些方法發(fā)送的 http 請求都將受益于 context.Context 附帶的 WithDeadline,WithCancel 和 WithValue 等方法支持。
一般有兩種方式能夠在支持 context.Context 的同時保持代碼的向后兼容:
在 struct 中添加 context (稍后我們將看到); 復制原有函數(shù),在函數(shù)第一個參數(shù)中使用 context,舉個栗子, database/sql這個 package 的Query方法的簽名一直是:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
當 context package 引入的時候,Go team 新增了這樣一個函數(shù):
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
并且只修改了一處代碼:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
通過這種方式,Go team 能夠在平滑地升級一個 package 的同時不對代碼的可讀性、兼容性造成影響。類似的代碼在 golang 源碼中隨處可見。更多的保持代碼兼容性的討論可見 [Go team 關于如何保持 Go Modules 兼容性的一些實踐]。
然而在某些情況下,比如 API 公開了大量 function,重寫所有函數(shù)是不切實際的。
package net/http 選擇在 struct 中添加 context.Context,這是一個結構體嵌套 context 比較恰當?shù)姆独?。先讓我們看?net/http 的 Do 函數(shù)。在引入 context 之前,Do 的定義如下:
func (c *Client) Do(req *Request) (*Response, error)
在 1.7 引入 context 后,為了遵循 net/http 這種標準庫的向后兼容原則,考慮到該核心庫所包含的函數(shù)過于多,maintainers 選擇在結構體 http.Request 中添加 context.Context。
type Request struct {
ctx context.Context
// ...
}
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// Simplified for brevity of this article.
return &Request{
ctx: ctx,
// ...
}
}
func (c *Client) Do(req *Request) (*Response, error)
在修改大量 API 以支持 context 時,在結構體中添加 context.Context 是有意義的, 如上所述。但是,記住首先要考慮復制函數(shù)對 context 進行支持,在不犧牲實用性和理解性的前提下向后兼容上下文:
func (c *Client) Call() error {
return c.CallContext(context.Background())
}
func (c *Client) CallContext(ctx context.Context) error {
// ...
}
Conclusion
通過 context,可以輕松地在調用堆棧中傳播重要的跨 lib 和跨 API 信息。但是,必須一致、清晰地使用它,以使其易于理解,易于調試且有效。
當 context 作為方法中的第一個參數(shù)傳遞而不是存儲在 struct 中時,用戶可以充分利用其可擴展性,可以通過調用堆棧構建 WithCancel,WithDeadline 和 WithValue 的傳遞樹。而且最重要的是,當將其作為參數(shù)傳入時,可以清楚地了解其傳播范圍,從而可以輕松地理解和調試。
最后一句話總結本文,When designing an API with context, remember the advice: pass context.Context in as an argument; don't store it in structs.
原文地址:https://blog.golang.org/context-and-structs
推薦閱讀
