「譯」上下文 Context 與結(jié)構(gòu)體 Struct
原文地址:https://blog.golang.org/context-and-structs
原文作者:Jean de Klerk, Matt T. Proud
譯者:Kevin
介紹
在許多 Go API 中,尤其是現(xiàn)代的 API 中,函數(shù)和方法的第一個(gè)參數(shù)通常是context.Context。上下文(Context)提供了一種方法,用于跨 API 邊界和進(jìn)程之間傳輸截止時(shí)間、調(diào)用者取消和其他請(qǐng)求范圍的值。當(dāng)一個(gè)庫(kù)與遠(yuǎn)程服務(wù)器(如數(shù)據(jù)庫(kù)、API 等)直接或間接交互時(shí),經(jīng)常會(huì)用到它。
在context 的文檔中寫道。
上下文不應(yīng)該存儲(chǔ)在結(jié)構(gòu)類型里面,而是傳遞給每個(gè)需要它的函數(shù)。
本文對(duì)這一建議進(jìn)行了擴(kuò)展,用具體例子解析為什么傳遞上下文而不是將其存儲(chǔ)在其他類型中很重要。它還強(qiáng)調(diào)了一種罕見(jiàn)的情況,即在結(jié)構(gòu)類型中存儲(chǔ)上下文可能是有意義的,以及如何安全地這樣做。
傾向于將上下文作為參數(shù)傳遞
為了深入理解不在結(jié)構(gòu)中存儲(chǔ)上下文的建議,我們來(lái)考慮一下首選的上下文作為參數(shù)的方法。
type Worker struct { /* … */ }
type Work struct { /* … */ }
func New() *Worker {
return &Worker{}
}
func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
_ = ctx // 每次調(diào)用中ctx用于取消操作,截止時(shí)間和元數(shù)據(jù)。
}
func (w *Worker) Process(ctx context.Context, w *Work) error {
_ = ctx // A每次調(diào)用中ctx用于取消操作,截止時(shí)間和元數(shù)據(jù)。
}
在這個(gè)例子中,(*Worker).Fetch和(*Worker).Process方法都直接接受上下文。通過(guò)這種通過(guò)參數(shù)傳遞的設(shè)計(jì),用戶可以設(shè)置每次調(diào)用的截止時(shí)間、取消和元數(shù)據(jù)。而且,很清楚傳遞給每個(gè)方法的context.Context將如何被使用:沒(méi)有期望傳遞給一個(gè)方法的context.Context將被任何其他方法使用。這是因?yàn)樯舷挛牡姆秶幌薅ㄔ诹诵》秶谋仨毑僮鲀?nèi),這大大增加了這個(gè)包中上下文的實(shí)用性和清晰度。
將上下文存儲(chǔ)在結(jié)構(gòu)中會(huì)導(dǎo)致混亂
讓我們?cè)俅问褂蒙舷挛拇鎯?chǔ)在結(jié)構(gòu)體中這種方式審視一下上面的Worker例子。它的問(wèn)題是,當(dāng)你把上下文存儲(chǔ)在一個(gè)結(jié)構(gòu)中時(shí),你會(huì)向調(diào)用者隱藏它的生命周期,甚至可能的是把兩個(gè)不同的作用域以不可預(yù)料的方式互相干擾:
type Worker struct {
ctx context.Context
}
func New(ctx context.Context) *Worker {
return &Worker{ctx: ctx}
}
func (w *Worker) Fetch() (*Work, error) {
_ = w.ctx // 共享的w.ctx用于取消操作,截止時(shí)間和元數(shù)據(jù)。
}
func (w *Worker) Process(w *Work) error {
_ = w.ctx // 共享的w.ctx用于取消操作,截止時(shí)間和元數(shù)據(jù)。
}
(*Worker).Fetch和(*Worker).Process方法都使用存儲(chǔ)在Worker中的上下文。這防止了Fetch和Process的調(diào)用者(它們本身可能有不同的上下文)在每次調(diào)用的基礎(chǔ)上指定截止日期、請(qǐng)求取消和附加元數(shù)據(jù)。例如:用戶無(wú)法只為(*Worker).Fetch提供截止日期,也無(wú)法只取消(*Worker).Process的調(diào)用。調(diào)用者的生命期與共享上下文交織在一起,上下文的范圍是創(chuàng)建Worker的生命周期。
與上下文作為參數(shù)的方法相比,該 API 也更容易讓用戶感到疑惑。用戶可能會(huì)問(wèn)自己:
既然 New需要一個(gè)context.Context,那么構(gòu)造函數(shù)是否在做取消或截止時(shí)間控制的工作?New傳遞進(jìn)來(lái)的context.Context是否適用于(*Worker).Fetch和(*Worker).Process?都不適用?有一個(gè)而沒(méi)有另一個(gè)?
API 需要大量的文檔來(lái)明確告訴用戶context.Context到底是用來(lái)做什么的。用戶可能還需要閱讀代碼,而不是能夠依靠 API 結(jié)構(gòu)獲得信息。
最后,如果設(shè)計(jì)一個(gè)生產(chǎn)級(jí)服務(wù)器,其每個(gè)請(qǐng)求沒(méi)有上下文,從而不能充分重視取消操作,這可能是相當(dāng)危險(xiǎn)的。如果沒(méi)有能力設(shè)置每個(gè)調(diào)用的截止日期,你的進(jìn)程可能會(huì)積壓資源并導(dǎo)致資源耗盡(如內(nèi)存)!
規(guī)則的例外:保存向后的兼容性
當(dāng)引入 context.Context的 Go 1.7 發(fā)布時(shí),大量的 API 必須以向后兼容的方式添加上下文支持。例如,net/http的Client方法,如Get和Do,就是很好的上下文取消操作的應(yīng)用。每一個(gè)用這些方法發(fā)送的外部請(qǐng)求都會(huì)受益于context.Context帶來(lái)的截止時(shí)間、取消和元數(shù)據(jù)支持。
有兩種方法可以以向后兼容的方式添加對(duì)context.Context的支持:將上下文包在一個(gè)結(jié)構(gòu)中,正如我們稍后將看到的那樣;復(fù)制函數(shù),復(fù)制的函數(shù)接受context.Context作為參數(shù),并將Context作為其函數(shù)名的后綴。復(fù)制的方法應(yīng)該比在結(jié)構(gòu)體中嵌入上下文的方式更可取,在保持模塊的兼容性中會(huì)進(jìn)一步討論。然而,在某些情況下,這是不切實(shí)際的:例如,如果你的 API 暴露了大量的函數(shù),那么復(fù)制所有的函數(shù)可能是不可行的。
net/http包選擇了上下文存儲(chǔ)在結(jié)構(gòu)體方式,這提供了一個(gè)有用的案例研究。讓我們看看net/http的Do方法。在引入context.Context之前,Do的定義如下:
func (c *Client) Do(req *Request) (*Response, error)
在 Go 1.7 之后,如果不考慮破壞向后的兼容性的問(wèn)題,Do 可能看起來(lái)像下面這樣:
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
但是,保留向后的兼容性,遵守Go 1 的兼容性承諾對(duì)于標(biāo)準(zhǔn)庫(kù)來(lái)說(shuō)是至關(guān)重要的。所以,維護(hù)者選擇在http.Request結(jié)構(gòu)上添加一個(gè)context.Context,以便在不破壞向后兼容性的情況下支持context.Context:
type Request struct {
ctx context.Context
// ...
}
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// 為了本文演示需要做了簡(jiǎn)化。
return &Request{
ctx: ctx,
// ...
}
}
func (c *Client) Do(req *Request) (*Response, error)
當(dāng)改造你的 API 以支持上下文時(shí),像上面那樣在一個(gè)結(jié)構(gòu)中添加一個(gè)context.Context可能是有意義的。但是,你需要首先考慮復(fù)制你的函數(shù),這樣可以在不犧牲實(shí)用性和理解性的前提下,向后兼容地改造context.Context。例如:
func (c *Client) Call() error {
return c.CallContext(context.Background())
}
func (c *Client) CallContext(ctx context.Context) error {
// ...
}
總結(jié)
上下文使得重要的跨庫(kù)和跨 API 信息很容易在調(diào)用棧中傳播。但是,為了保持可理解性、易調(diào)試性和有效性,必須統(tǒng)一清晰地使用它。
當(dāng)作為方法中的第一個(gè)參數(shù)而不是存儲(chǔ)在結(jié)構(gòu)類型中時(shí),用戶可以充分利用它的可擴(kuò)展性,以便通過(guò)調(diào)用棧建立一個(gè)強(qiáng)大的取消、截止日期和元數(shù)據(jù)信息樹。而且,最重要的是,當(dāng)它作為一個(gè)參數(shù)傳遞進(jìn)來(lái)時(shí),它的范圍被清晰的理解,從而導(dǎo)致堆棧上下的理解更加清晰和調(diào)試更加容易。
當(dāng)設(shè)計(jì)一個(gè)帶有上下文的 API 時(shí),請(qǐng)記住這樣的建議:將context.Context作為一個(gè)參數(shù)傳遞進(jìn)來(lái),不要將它存儲(chǔ)在結(jié)構(gòu)體中。
