一缠犀、Context概述
1.緣起
在開發(fā)web服務(wù)應(yīng)用時浇揩,我們知道http啟動的服務(wù)每接收到一個請求是便啟動一個goroutine處理該request寡夹。而每個協(xié)程處理該請求是一般都會啟動多個協(xié)程去處理不同任務(wù)匾嘱,如調(diào)用RPC饺谬、訪問數(shù)據(jù)庫資源嫩海、緩存資源等等冬殃,這些協(xié)程都是為處理同一個request工作的,同時當(dāng)request被取消或者超時的時候叁怪,從這個request處理協(xié)程創(chuàng)建的所有子協(xié)程也應(yīng)該被結(jié)束审葬。此時一個handler就必須對其啟動的子協(xié)程有控制權(quán),在context出現(xiàn)前奕谭,上述那些處理還是很丑陋的涣觉,有些甚至引起全局資源的濫用或者回調(diào)噩夢。context出現(xiàn)后血柳,一切都得到解脫官册,context解決了處理同一生命周期協(xié)程樹的資源管理問題。
2.官方解釋:
Context难捌,翻譯為“上下文”膝宁,context包定義了Context接口類型,其接口簽名方法定義了跨API邊界和進(jìn)程之間的執(zhí)行最后期限根吁、取消信號和其他請求范圍的值员淫。
對服務(wù)器的傳入請求應(yīng)創(chuàng)建Context類型,對服務(wù)器的傳出調(diào)用應(yīng)接受Context击敌。它們之間的函數(shù)調(diào)用鏈必須傳播Context介返,可以選擇將其替換為使用WithCancel()、WithDeadline()愚争、WithTimeout()或WithValue()創(chuàng)建的派生Context映皆。當(dāng)一個context被取消時挤聘,從它派生的所有context也被取消。WithCancel()捅彻、WithDeadline()和WithTimeout()函數(shù)接受上下文(父級)并返回派生上下文(子級)和Cancelfunc组去。調(diào)用Cancelfunc將取消子級及其子孫級,刪除父級對子級的引用步淹,并停止任何關(guān)聯(lián)的計時器从隆。如果不調(diào)用Cancelfunc,則會泄漏子級及其子孫級缭裆,直到父級被取消或計時器觸發(fā)键闺。Go-Vet工具檢查取消功能是否用于所有控制流路徑。
使用Context的程序應(yīng)該遵循這些規(guī)則澈驼,以保持包之間的接口一致辛燥,并允許靜態(tài)分析工具檢查上下文傳播:
//context傳遞的寫法
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
不要將Context存儲在結(jié)構(gòu)類型中;而是將Context顯式傳遞給每個需要它的函數(shù)缝其。文應(yīng)該是第一個參數(shù)挎塌,通常名為ctx:
即使函數(shù)允許,也不要傳遞nil上下文内边。如果不確定要使用哪個上下文榴都,請傳遞context.TODO(),該函數(shù)返回一個可被跟蹤的頂級Context漠其。
只對傳輸進(jìn)程和API的請求范圍數(shù)據(jù)使用Context值嘴高,而不用于向函數(shù)傳遞可選參數(shù)。
同一Context可以傳遞給在不同goroutine中運(yùn)行的函數(shù)和屎;上下文對于多個goroutine同時使用是安全的拴驮。
3.context包解析
我們來看一下Context接口的簽名方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
3.1 Context接口簽名方法解析:
- Deadline() (deadline time.Time, ok bool)
Deadline方法返回應(yīng)取消代表此上下文完成的工作的時間。
- 未設(shè)置截止時間時柴信,Deadline方法返回ok==false莹汤。
- 對Deadline方法的連續(xù)調(diào)用將返回相同的結(jié)果。
- Done() <-chan struct{}
done返回一個通道颠印,該通道在應(yīng)取消代表此上下文完成的工作時關(guān)閉。如果無法取消此上下文抹竹,則done可能返回nil线罕。對done的連續(xù)調(diào)用返回相同的值。不同的派生Context對done通道關(guān)閉有不同的處理方式:
- WithCancel()在調(diào)用cancel時安排關(guān)閉done窃判;
- WithDeadline()在截止時間過期時安排關(guān)閉done钞楼;
- WithTimeout()在超時結(jié)束時安排關(guān)閉done。
- Err() error
Err方法在done關(guān)閉后返回非零錯誤值袄琳。
返回值:
- 如果上下文被取消询件,則返回Canceled燃乍;如果上下文的截止時間已過,則返回DeadLineExceeded宛琅;
- 沒有為err定義其他值刻蟹。
- 完成后關(guān)閉,對err的連續(xù)調(diào)用將返回相同的值嘿辟。
- Value(key interface{}) interface{}
- 該方法可以讓協(xié)程共享一些數(shù)據(jù)舆瘪,獲得數(shù)據(jù)是協(xié)程安全的。
- 該方法返回與鍵的上下文關(guān)聯(lián)的值红伦,如果沒有值與鍵關(guān)聯(lián)英古,則返回nil。
- 對具有相同鍵的值的連續(xù)調(diào)用返回相同的結(jié)果昙读。
僅對傳輸進(jìn)程和API邊界的請求范圍數(shù)據(jù)使用上下文值召调,而不用于向函數(shù)傳遞可選參數(shù)。
鍵標(biāo)識上下文中的特定值蛮浑。希望在上下文中存儲值的函數(shù)通常在全局變量中分配一個鍵唠叛,然后使用該鍵作為context.WithValue() 和 Context.Value的參數(shù)。鍵可以是支持相等的任何類型陵吸;包應(yīng)將鍵定義為未排序的類型以避免沖突玻墅。
3.2 頂級Context
context包提供兩種頂級的上下文類型,由工廠方法創(chuàng)建:
(1).func Background() Context
context.Background()返回非零的空上下文壮虫。它從不被取消澳厢,沒有值,也沒有最后期限囚似。它通常由主函數(shù)剩拢、初始化和測試使用,并且作為傳入請求的頂級上下文饶唤。
(2).func TODO() Context
context.TODO()返回非零的空上下文徐伐。當(dāng)不清楚要使用哪個上下文或者它還不可用時(因為周圍的函數(shù)還沒有被擴(kuò)展以接受上下文參數(shù)),應(yīng)該使用context.TODO()募狂。靜態(tài)分析工具可以識別TODO办素,它確定上下文是否在程序中正確傳播。
兩者區(qū)別:
==本質(zhì)來講兩者區(qū)別不大祸穷,其源碼實現(xiàn)是一樣的性穿,只不過使用場景不同,context.Background()通常由主函數(shù)雷滚、初始化和測試使用需曾,是頂級Context;context.TODO()通常用于主協(xié)程外的其他協(xié)程向下傳遞,分析工具可識別它在調(diào)用棧中傳播呆万。==
3.3 派生Context
除以上兩種頂級Context類型商源,context包提供四種創(chuàng)建可派生Context類型的函數(shù):
(1). func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel函數(shù)返回具有新done通道的父級副本。當(dāng)調(diào)用返回的cancel函數(shù)或關(guān)閉父上下文的done通道時(以先發(fā)生者為準(zhǔn))谋减,將關(guān)閉返回的上下文的done通道牡彻。
取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用Cancel逃顶。
官方使用示例:
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
//OUTPUT:
1
2
3
4
5
(2). func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
WithDeadline函數(shù)返回父上下文的副本讨便,其截止時間調(diào)整為不遲于d。如果父上下文的截止時間早于d以政,則WithDeadline(Parent霸褒,d)在語義上等同于父上下文。當(dāng)截止時間到期盈蛮、調(diào)用返回的cancel函數(shù)或關(guān)閉父上下文的done通道(以先發(fā)生者為準(zhǔn))時废菱,返回的上下文的done通道將關(guān)閉。
取消此上下文將釋放與其關(guān)聯(lián)的資源抖誉,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用Cancel殊轴。
官方使用示例:
這個例子傳遞一個具有任意截止時間的上下文,告訴一個阻塞函數(shù)一旦到達(dá)它就應(yīng)該放棄它的工作袒炉。
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
//OUTPUT:
context deadline exceeded
(3). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))旁理。取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用取消:
官方使用示例:這個例子傳遞一個帶有超時的上下文我磁,告訴一個阻塞函數(shù)它應(yīng)該在超時結(jié)束后放棄它的工作孽文。
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
//OUTPUT:
context deadline exceeded
*以上函數(shù)的特殊返回類型值:type CancelFunc func()
CancelFunc告訴操作放棄其工作。CancelFunc不等待工作停止夺艰。在第一次調(diào)用之后芋哭,對CancelFunc的后續(xù)調(diào)用不做任何操作。
(4). func WithValue(parent Context, key, val interface{}) Context
WithValue返回父級的副本郁副,可為上下文設(shè)置一個鍵值對减牺。
只對傳輸進(jìn)程和API的請求范圍數(shù)據(jù)使用上下文值,而不用于向函數(shù)傳遞可選參數(shù)存谎。
提供的鍵必須是可比較的拔疚,并且不應(yīng)是字符串或任何其他內(nèi)置類型龙优,以避免使用上下文的包之間發(fā)生沖突世杀。WithValue的用戶應(yīng)該為鍵定義自己的類型。為了避免在分配給接口時進(jìn)行分配赃梧,上下文鍵通常具有具體的類型結(jié)構(gòu)固以。或者,導(dǎo)出的上下文鍵變量的靜態(tài)類型應(yīng)該是指針或接口憨琳。
官方使用示例:
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
//OUTPUT:
found value: Go
key not found: color
三诫钓、Context使用示例
使用context
包來實現(xiàn)線程安全退出或超時的控制:
//定義一個并發(fā)worker
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
//當(dāng)父協(xié)程調(diào)用cancel()時,會從ctx.Done()得到struct{}篙螟,此時返回ctx.Err()退出子線程
case <-ctx.Done():
return ctx.Err()
default:
//默認(rèn)輸出hello
fmt.Println("hello")
}
}
}
func main() {
//生成一個有超時控制的衍生Context菌湃,超時10s退出所有子協(xié)程
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
//主協(xié)程1s后就cancel所有子協(xié)程了,每個worker都可以安全退出
time.Sleep(time.Second)
cancel()
wg.Wait()
}
當(dāng)并發(fā)體超時或main
主動停止工作者Goroutine時遍略,每個工作者都可以安全退出惧所。
Go語言是帶內(nèi)存自動回收特性的,因此內(nèi)存一般不會泄漏绪杏。當(dāng)main
函數(shù)不再使用管道時后臺Goroutine有泄漏的風(fēng)險下愈。我們可以通過context
包來避免這個問題,下面是防止內(nèi)存泄露的素數(shù)篩實現(xiàn):
// 返回生成自然數(shù)序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
select {
//父協(xié)程cancel()時安全退出該子協(xié)程
case <- ctx.Done():
return
//生成的素數(shù)發(fā)送到管道
case ch <- i:
}
}
}()
return ch
}
// 管道過濾器: 刪除能被素數(shù)整除的數(shù)
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
select {
//父協(xié)程cancel()時安全退出該子協(xié)程
case <- ctx.Done():
return
case out <- i:
}
}
}
}()
return out
}
func main() {
// 使用一個可由父協(xié)程控制子協(xié)程安全退出的Context蕾久。
ctx, cancel := context.WithCancel(context.Background())
ch := GenerateNatural(ctx) // 自然數(shù)序列: 2, 3, 4, ...
for i := 0; i < 100; i++ {
// 新出現(xiàn)的素數(shù)打印出來
prime := <-ch
fmt.Printf("%v: %v\n", i+1, prime)
// 基于新素數(shù)構(gòu)造的過濾器
ch = PrimeFilter(ctx, ch, prime)
}
//輸出100以內(nèi)符合要求的素數(shù)后安全退出所有子協(xié)程
cancel()
}
當(dāng)main函數(shù)完成工作前势似,通過調(diào)用cancel()
來通知后臺Goroutine退出,這樣就避免了Goroutine的泄漏僧著。