The Go Blog: 關(guān)于 context 的一點最佳實踐
關(guān)于 context 的一點最佳實踐

2021 年 2月 24 日,官方 blog 中詳細(xì)講了關(guān)于 context 使用的一些最佳實踐,提供了代碼示例,告訴你為何 context不應(yīng)存儲在 struct 內(nèi)部,最好的方式是作為函數(shù)的第一個參數(shù)傳遞,以及如何在非常必要的情況下(保持向后兼容)以一種最安全的方式將 context 存儲到 struct 中。下面是原文的主要內(nèi)容。
Introduction
在許多 Go API,尤其是現(xiàn)代 API 中,函數(shù)和方法的第一個參數(shù)通常是 context.Context。context 提供了很多方法例如 WithCancel、WithDeadline、WithValue、以實現(xiàn)跨 API 的流程控制。很多 lib 在與遠(yuǎn)程服務(wù)器(如數(shù)據(jù)庫、api等)交互時,經(jīng)常使用 context 做控制。
context 的文檔中講到:
?context?不應(yīng)存儲在?struct?內(nèi)部,最好的方式是作為函數(shù)的第一個參數(shù)傳遞
本文在此建議的基礎(chǔ)上講解了原因和示例,說明了為什么要使用函數(shù)傳遞 Context 而不是將其存儲在 struct 中的重要性。還以 net/http 的代碼為例,解釋了應(yīng)該在何種情況下可以在 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 的生成到結(jié)束,調(diào)用方可以很清晰地知道 context 的傳遞路線。
將 context 存儲到 strcut 所帶來的一些困惑
在結(jié)構(gòu)體中嵌套 context 來實現(xiàn)上面的 Worker 示例,調(diào)用者對所使用的 context 生命周期產(chǎn)生迷惑:
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 結(jié)構(gòu)體中的 context ,這種情況下使得調(diào)用方無法定義不同的 context ,比如有調(diào)用方想用 WitchCancel,有的想用 WithDeadline,也很難理解上面?zhèn)鱽淼?context 的作用是 cancel?還是 deadline?調(diào)用者所使用 context 的生命周期被綁定到了一個共享的 context 上面。
特殊情況:保留向后兼容性
當(dāng) go 1.7 版本發(fā)布時,大量的的 API 需要以向后兼容的方式支持 context.Context,例如,net/http 的 Client 方法(例如Get和Do)是使用 context 的典范。使用這些方法發(fā)送的 http 請求都將受益于 context.Context 附帶的 WithDeadline,WithCancel 和 WithValue 等方法支持。
一般有兩種方式能夠在支持 context.Context 的同時保持代碼的向后兼容:
- 在 struct 中添加 context (稍后我們將看到);
- 復(fù)制原有函數(shù),在函數(shù)第一個參數(shù)中使用 context,舉個栗子,
database/sql這個 package 的Query方法的簽名一直是:
func?(db?*DB)?Query(query?string,?args?...interface{})?(*Rows,?error)
當(dāng) 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 關(guān)于如何保持 Go Modules 兼容性的一些實踐]。
然而在某些情況下,比如 API 公開了大量 function,重寫所有函數(shù)是不切實際的。
package net/http 選擇在 struct 中添加 context.Context,這是一個結(jié)構(gòu)體嵌套 context 比較恰當(dāng)?shù)姆独?。先讓我們看?net/http 的 Do 函數(shù)。在引入 context 之前,Do 的定義如下:
func?(c?*Client)?Do(req?*Request)?(*Response,?error)
在 1.7 引入 context 后,為了遵循 net/http 這種標(biāo)準(zhǔn)庫的向后兼容原則,考慮到該核心庫所包含的函數(shù)過于多,maintainers 選擇在結(jié)構(gòu)體 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 時,在結(jié)構(gòu)體中添加 context.Context 是有意義的, 如上所述。但是,記住首先要考慮復(fù)制函數(shù)對 context 進(jìn)行支持,在不犧牲實用性和理解性的前提下向后兼容上下文:
func?(c?*Client)?Call()?error?{
??return?c.CallContext(context.Background())
}
func?(c?*Client)?CallContext(ctx?context.Context)?error?{
??//?...
}
Conclusion
通過 context,可以輕松地在調(diào)用堆棧中傳播重要的跨 lib 和跨 API 信息。但是,必須一致、清晰地使用它,以使其易于理解,易于調(diào)試且有效。
當(dāng) context 作為方法中的第一個參數(shù)傳遞而不是存儲在 struct 中時,用戶可以充分利用其可擴展性,可以通過調(diào)用堆棧構(gòu)建 WithCancel,WithDeadline 和 WithValue 的傳遞樹。而且最重要的是,當(dāng)將其作為參數(shù)傳入時,可以清楚地了解其傳播范圍,從而可以輕松地理解和調(diào)試。
最后一句話總結(jié)本文,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
官方資訊*最新技術(shù)*獨家解讀
