一篇文章搞懂Go語言中的Context
0 前置知識(shí)sync.WaitGroup
sync.WaitGroup是等待一組協(xié)程結(jié)束。它實(shí)現(xiàn)了一個(gè)類似任務(wù)隊(duì)列的結(jié)構(gòu),可以向隊(duì)列中加入任務(wù),任務(wù)完成后就把任務(wù)從隊(duì)列中移除,如果隊(duì)列中的任務(wù)沒有全部完成,隊(duì)列就會(huì)觸發(fā)阻塞以阻止程序繼續(xù)運(yùn)行。 sync.WaitGroup只有3個(gè)方法,Add(),Done(),Wait() 。
其中Done()是Add(-1)的別名,使用Add()添加計(jì)數(shù),Done()減掉一個(gè)計(jì)數(shù),計(jì)數(shù)不為0, 阻塞Wait()的運(yùn)行。
示例:
package main import ( ? "fmt" ? "sync" ? "time" ) var group sync.WaitGroup func sayHello() { ? for i := 0; i < 5; i++ { ? ? ?fmt.Println("hello......") ? ? ?time.Sleep(time.Second) ? } ? //線程結(jié)束 -1 ? group.Done() } func sayHi() { ? //線程結(jié)束 -1 ? defer group.Done() ? for i := 0; i < 5; i++ { ? ? ?fmt.Println("hi......") ? ? ?time.Sleep(time.Second) ? } } func main() { ? //+2 ? group.Add(2) ? fmt.Println("main正在阻塞...") ? go sayHello() ? fmt.Println("main持續(xù)阻塞...") ? go sayHi() ? //線程等待 ? group.Wait() ? fmt.Println("main貌似結(jié)束了阻塞...") }
效果:
1 簡(jiǎn)介
在 Go 服務(wù)器中,每個(gè)傳入請(qǐng)求都在其自己的 goroutine 中處理。請(qǐng)求處理程序通常會(huì)啟動(dòng)額外的 goroutine 來訪問后端,例如數(shù)據(jù)庫和 RPC 服務(wù)。處理請(qǐng)求的一組 goroutine 通常需要訪問特定于請(qǐng)求的值,例如最終用戶的身份、授權(quán)令牌和請(qǐng)求的截止日期。當(dāng)請(qǐng)求被取消或超時(shí)時(shí),處理該請(qǐng)求的所有 goroutine 都應(yīng)該快速退出,以便系統(tǒng)可以回收它們正在使用的任何資源。
為此,開發(fā)了一個(gè)context
包,可以輕松地將請(qǐng)求范圍的值、取消信號(hào)和截止日期跨 API 邊界傳遞給處理請(qǐng)求所涉及的所有 goroutine。
Context攜帶一個(gè)截止日期、一個(gè)取消信號(hào)和其他跨越API邊界的值。上下文的方法可以被多個(gè)gor例程同時(shí)調(diào)用。
對(duì)服務(wù)器的傳入請(qǐng)求應(yīng)該創(chuàng)建一個(gè)上下文,對(duì)服務(wù)器的傳出調(diào)用應(yīng)該接受一個(gè)上下文。它們之間的函數(shù)調(diào)用鏈必須傳播 Context,可選擇將其替換為使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 創(chuàng)建的派生 Context。當(dāng)一個(gè)上下文被取消時(shí),所有從它派生的上下文也被取消。
WithCancel、WithDeadline 和 WithTimeout 函數(shù)采用 Context(父)并返回派生的 Context(子)和 CancelFunc。調(diào)用 CancelFunc 會(huì)取消子項(xiàng)及其子項(xiàng),刪除父項(xiàng)對(duì)子項(xiàng)的引用,并停止任何關(guān)聯(lián)的計(jì)時(shí)器。調(diào)用 CancelFunc 失敗會(huì)泄漏子項(xiàng)及其子項(xiàng),直到父項(xiàng)被取消或計(jì)時(shí)器觸發(fā)。go vet 工具檢查是否在所有控制流路徑上使用了 CancelFuncs。
使用上下文的程序應(yīng)遵循以下規(guī)則,以保持跨包的接口一致,并啟用靜態(tài)分析工具來檢查上下文傳播:
不要將上下文存儲(chǔ)在結(jié)構(gòu)類型中;相反,將 Context 顯式傳遞給需要它的每個(gè)函數(shù)。
Context 應(yīng)該是第一個(gè)參數(shù),通常命名為 ctx:
func DoSomething(ctx context.Context, arg Arg) error { // ... 使用 ctx ... }
即使函數(shù)允許,也不要傳遞 nil 上下文。如果不確定要使用哪個(gè) Context,請(qǐng)傳遞 context.TODO。
僅將上下文值用于傳輸流程和 API 的請(qǐng)求范圍數(shù)據(jù),而不用于將可選參數(shù)傳遞給函數(shù)。
相同的 Context 可以傳遞給在不同的 goroutine 中運(yùn)行的函數(shù);上下文對(duì)于多個(gè) goroutine 同時(shí)使用是安全的。
2 context.Context引入
//上下文攜帶截止日期、取消信號(hào)和請(qǐng)求范圍的值在API的界限。它的方法是安全的同時(shí)使用多個(gè)了goroutine。 type Context interface { ? ?// Done返回一個(gè)在上下文被取消或超時(shí)時(shí)關(guān)閉的通道。 ? ?Done() <-chan struct{} ? ? ?// Err表示在Done通道關(guān)閉后為何取消此上下文。 ? ?Err() error ? ? ?// Deadline返回上下文將被取消的時(shí)間(如果有的話)。 ? ?Deadline() (deadline time.Time, ok bool) ? ? ?// Value返回與key相關(guān)的值,如果沒有則返回nil。 ? ?Value(key interface{}) interface{} }
- 該
Done
方法返回一個(gè)通道,該通道作為代表運(yùn)行的函數(shù)的取消信號(hào)Context
:當(dāng)通道關(guān)閉時(shí),函數(shù)應(yīng)該放棄它們的工作并返回。 - 該
Err
方法返回一個(gè)錯(cuò)誤,指示Context
取消的原因。 - 一個(gè)
Context
對(duì)于多個(gè) goroutine 同時(shí)使用是安全的。代碼可以將單個(gè)傳遞Context
給任意數(shù)量的 goroutines 并取消它Context
以向所有g(shù)oroutine 發(fā)出信號(hào)。 - 該
Deadline
方法允許函數(shù)確定它們是否應(yīng)該開始工作,還可以使用截止日期來設(shè)置 I/O 操作的超時(shí)時(shí)間。 Value
允許一個(gè)Context
攜帶請(qǐng)求范圍的數(shù)據(jù)。該數(shù)據(jù)必須是安全的,以便多個(gè) goroutine 同時(shí)使用。
3 context包的其他常用函數(shù)
3.1 context.Background和context.TODO
Background是任何Context樹的根,它永遠(yuǎn)不會(huì)被取消:
//Background返回一個(gè)空的Context。 它永遠(yuǎn)不會(huì)取消,沒有截止日期,沒有價(jià)值。 Background通常用于main、init和tests,并作為傳入請(qǐng)求的頂級(jí)上下文。 ? func Background() Context
給一個(gè)函數(shù)方法傳遞Context的時(shí)候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()
3.2 context.WithCancel和
WithCancelt返回派生的Context值,可以比父Context更快地取消。當(dāng)請(qǐng)求處理程序返回時(shí),通常會(huì)取消與傳入請(qǐng)求關(guān)聯(lián)的content。當(dāng)使用多個(gè)副本時(shí),WithCancel對(duì)于取消冗余請(qǐng)求也很有用。
// WithCancel返回一個(gè)父進(jìn)程的副本,該父進(jìn)程的Done通道被盡快關(guān)閉。?關(guān)閉Done或調(diào)用cancel。 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // CancelFunc取消一個(gè)上下文。 type CancelFunc func()
示例:
package main import ( ? "context" ? "fmt" ) func play(ctx context.Context) <-chan int { ? dist := make(chan int) ? n := 1 ? //匿名函數(shù) 向dist中加入元素 ? go func() { ? ? ?for { ? ? ? ? select { ? ? ? ? //ctx為空時(shí)將不會(huì)執(zhí)行這個(gè) ? ? ? ? case <-ctx.Done(): ? ? ? ? ? ?return // return結(jié)束該goroutine,防止泄露 ? ? ? ? ? ?//向dist中加入元素 ? ? ? ? case dist <- n: ? ? ? ? ? ?n++ ? ? ? ? } ? ? } ? }() ? return dist } func main() { ? //返回空的context ? ctx, cancel := context.WithCancel(context.Background()) ? defer cancel() // 調(diào)用cancel ? for n := range play(ctx) { ? ? ?fmt.Println(n) ? ? ?if n == 5 { ? ? ? ? break ? ? } ? } }
擴(kuò)展:go中select的用法
``` select的用法與switch語言非常類似,由select開始一個(gè)新的選擇塊,每個(gè)選擇條件由case語句來描述。 與switch語句相比, select有比較多的限制,其中最大的一條限制就是每個(gè)case語句里必須是一個(gè)IO操作,大致的結(jié)構(gòu)如下: ``` go select { ? case <-chan1: ? ? ?// 如果chan1成功讀到數(shù)據(jù),則進(jìn)行該case處理語句 ? case chan2 <- 1: ? ? ?// 如果成功向chan2寫入數(shù)據(jù),則進(jìn)行該case處理語句 ? default: ? ? ?// 如果上面都沒有成功,則進(jìn)入default處理流程 } ``` 在一個(gè)select語句中,Go語言會(huì)按順序從頭至尾評(píng)估每一個(gè)發(fā)送和接收的語句。 如果其中的任意一語句可以繼續(xù)執(zhí)行(即沒有被阻塞),那么就從那些可以執(zhí)行的語句中任意選擇一條來使用。 如果沒有任意一條語句可以執(zhí)行(即所有的通道都被阻塞),那么有兩種可能的情況: - 如果給出了default語句,那么就會(huì)執(zhí)行default語句,同時(shí)程序的執(zhí)行會(huì)從select語句后的語句中恢復(fù)。 - 如果沒有default語句,那么select語句將被阻塞,直到至少有一個(gè)通信可以進(jìn)行下去。 ```
3.3 context.WithTimeout
WithTimeout返回派生的Context值,WithTimeout用于設(shè)置請(qǐng)求到后端服務(wù)器的截止日期:
//WithTimeout返回一個(gè)父進(jìn)程的副本,該父進(jìn)程的Done通道被立即關(guān)閉的父母。關(guān)閉“完成”、調(diào)用“取消”或超時(shí)結(jié)束。新 //Context的Deadline是現(xiàn)在的更快+timeout和父的Deadline,如果任何。?如果計(jì)時(shí)器仍然在運(yùn)行,則cancel函數(shù)釋放它資源。 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // CancelFunc取消一個(gè)上下文。 type CancelFunc func()
示例:
package main import ( ? "context" ? "fmt" ? "sync" ? "time" ) var wg sync.WaitGroup func worker(ctx context.Context) { ? ?LOOP: ? for { ? ? ?fmt.Println("db connecting ...") ? ? ?time.Sleep(time.Millisecond * 10) // 假設(shè)正常連接數(shù)據(jù)庫耗時(shí)10毫秒 ? ? ?select { ? ? ?case <-ctx.Done(): // 50毫秒后自動(dòng)調(diào)用 ? ? ? ? break LOOP ? ? ?default: ? ? } ? } ? fmt.Println("worker done!") ? wg.Done() } func main() { ? // 設(shè)置一個(gè)50毫秒的超時(shí) ? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) ? wg.Add(1) ? go worker(ctx) ? time.Sleep(time.Second * 5) ? cancel() // 通知子goroutine結(jié)束 ? wg.Wait() ? fmt.Println("over") }
執(zhí)行結(jié)果:
3.4 context.WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ? if parent == nil { ? ? ?panic("cannot create context from nil parent") ? } ? if cur, ok := parent.Deadline(); ok && cur.Before(d) { ? ? ?// 目前的期限已經(jīng)比新的期限提前 ? ? ?return WithCancel(parent) ? } ? c := &timerCtx{ ? ? ?cancelCtx: newCancelCtx(parent), ? ? ?deadline: ?d, ? } ? propagateCancel(parent, c) ? dur := time.Until(d) ? if dur <= 0 { ? ? ?c.cancel(true, DeadlineExceeded) // 截止日期已經(jīng)過去了 ? ? ?return c, func() { c.cancel(false, Canceled) } ? } ? c.mu.Lock() ? defer c.mu.Unlock() ? if c.err == nil { ? ? ?c.timer = time.AfterFunc(dur, func() { ? ? ? ? c.cancel(true, DeadlineExceeded) ? ? }) ? } ? return c, func() { c.cancel(true, Canceled) } }
示例:
package main import ( ? "context" ? "fmt" ? "time" ) func main() { ? d := time.Now().Add(500 * time.Millisecond) ? ctx, cancel := context.WithDeadline(context.Background(), d) ? // 盡管ctx會(huì)過期,但在任何情況下調(diào)用它的cancel函數(shù)都是很好的實(shí)踐。 ? // 如果不這樣做,可能會(huì)使上下文及其父類存活的時(shí)間超過必要的時(shí)間。 ? defer cancel() ? select { ? case <-time.After(1 * time.Second): ? ? ?fmt.Println("over") ? case <-ctx.Done(): ? ? ?fmt.Println(ctx.Err()) ? } }
執(zhí)行結(jié)果:
3.5 context.WithValue
WithValue提供了一種將請(qǐng)求范圍的值與Context關(guān)聯(lián)的方法 :
//WithValue返回父元素的副本,其Value方法返回val for key。 func WithValue(parent Context, key interface{}, val interface{}) Context
了解如何使用context包的最好方法是通過一個(gè)已工作的示例。
示例:
package main import ( ? "context" ? "fmt" ? "sync" ? "time" ) type TraceCode string var wg sync.WaitGroup func worker(ctx context.Context) { ? key := TraceCode("KEY_CODE") ? traceCode, ok := ctx.Value(key).(string) // 在子goroutine中獲取trace code ? if !ok { ? ? ?fmt.Println("invalid trace code") ? } ? ?LOOP: ? for { ? ? ?fmt.Printf("worker,code:%s\n", traceCode) ? ? ?time.Sleep(time.Millisecond * 10) // 假設(shè)正常連接數(shù)據(jù)庫耗時(shí)10毫秒 ? ? ?select { ? ? ?case <-ctx.Done(): // 50毫秒后自動(dòng)調(diào)用 ? ? ? ? break LOOP ? ? ?default: ? ? } ? } ? fmt.Println("worker is over!") ? wg.Done() } ? func main() { ? // 設(shè)置一個(gè)50毫秒的超時(shí) ? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) ? // 在系統(tǒng)的入口中設(shè)置trace code傳遞給后續(xù)啟動(dòng)的goroutine實(shí)現(xiàn)日志數(shù)據(jù)聚合 ? ctx = context.WithValue(ctx, TraceCode("KEY_CODE"), "12512312234") ? wg.Add(1) ? go worker(ctx) ? time.Sleep(time.Second * 5) ? cancel() // 通知子goroutine結(jié)束 ? wg.Wait() ? fmt.Println("over") }
執(zhí)行結(jié)果:
4 實(shí)例:請(qǐng)求瀏覽器超時(shí)
server端:
package main import ( ? "fmt" ? "math/rand" ? "net/http" ? "time" ) // server端,隨機(jī)出現(xiàn)慢響應(yīng) func indexHandler(w http.ResponseWriter, r *http.Request) { ? number := rand.Intn(2) ? if number == 0 { ? ? ?time.Sleep(time.Second * 10) // 耗時(shí)10秒的慢響應(yīng) ? ? ?fmt.Fprintf(w, "slow response") ? ? ?return ? } ? fmt.Fprint(w, "quick response") } func main() { ? http.HandleFunc("/", indexHandler) ? err := http.ListenAndServe(":9999", nil) ? if err != nil { ? ? ?panic(err) ? } }
client端:
package main import ( ? "context" ? "fmt" ? "io/ioutil" ? "net/http" ? "sync" ? "time" ) // 客戶端 ? type respData struct { ? resp *http.Response ? err ?error } func doCall(ctx context.Context) { ? // http長連接 ? transport := http.Transport{DisableKeepAlives: true} ? client := http.Client{Transport: &transport} ? ? respChan := make(chan *respData, 1) ? req, err := http.NewRequest("GET", "http://127.0.0.1:9999/", nil) ? if err != nil { ? ? ?fmt.Println(err) ? ? ?return ? } ? req = req.WithContext(ctx) // 使用帶超時(shí)的ctx創(chuàng)建一個(gè)新的client request ? var wg sync.WaitGroup ? wg.Add(1) ? defer wg.Wait() ? go func() { ? ? ?resp, err := client.Do(req) ? ? ?fmt.Printf("resp:%v, err:%v\n", resp, err) ? ? ?rd := &respData{ ? ? ? ? resp: resp, ? ? ? ? err: ?err, ? ? } ? ? ?respChan <- rd ? ? ?wg.Done() ? }() ? select { ? case <-ctx.Done(): ? ? ?fmt.Println("timeout...") ? case result := <-respChan: ? ? ?fmt.Println("success....") ? ? ?if result.err != nil { ? ? ? ? fmt.Printf("err:%v\n", result.err) ? ? ? ? return ? ? } ? ? ?defer result.resp.Body.Close() ? ? ?data, _ := ioutil.ReadAll(result.resp.Body) ? ? ?fmt.Printf("resp:%v\n", string(data)) ? } } ? func main() { ? // 定義一個(gè)100毫秒的超時(shí) ? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) ? defer cancel() // 調(diào)用cancel釋放子goroutine資源 ? doCall(ctx) }
5 Context包都在哪些地方使用
許多服務(wù)器框架提供了用于承載請(qǐng)求作用域值的包和類型。我們可以定義“Context”接口的新實(shí)現(xiàn),在使用現(xiàn)有框架的代碼和需要“Context”參數(shù)的代碼之間架起橋梁。
6 小結(jié)
在谷歌中,要求Go程序員將“Context”參數(shù)作為傳入和傳出請(qǐng)求之間的調(diào)用路徑上的每個(gè)函數(shù)的第一個(gè)參數(shù)傳遞。這使得許多不同團(tuán)隊(duì)開發(fā)的Go代碼能夠很好地互操作。它提供了對(duì)超時(shí)和取消的簡(jiǎn)單控制,并確保像安全憑證這樣的關(guān)鍵值能夠正確地傳輸Go程序。
想要構(gòu)建在“Context”上的服務(wù)器框架應(yīng)該提供“Context”的實(shí)現(xiàn)來連接它們的包和那些需要“Context”參數(shù)的包。然后,它們的客戶端庫將接受來自調(diào)用代碼的“Context”。通過為請(qǐng)求范圍的數(shù)據(jù)和取消建立一個(gè)公共接口,“上下文”使包開發(fā)人員更容易共享創(chuàng)建可伸縮服務(wù)的代碼。
到此這篇關(guān)于一篇文章搞懂Go語言中的Context的文章就介紹到這了,更多相關(guān) Go Context內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文教你如何快速學(xué)會(huì)Go的切片和數(shù)組數(shù)據(jù)類型
數(shù)組是屬于同一類型的元素的集合。切片是數(shù)組頂部的方便、靈活且功能強(qiáng)大的包裝器。本文就來和大家聊聊Go中切片和數(shù)組的使用,需要的可以參考一下2023-03-03go獲取協(xié)程(goroutine)號(hào)的實(shí)例
這篇文章主要介紹了go獲取協(xié)程(goroutine)號(hào)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12GoLand一鍵上傳項(xiàng)目到遠(yuǎn)程服務(wù)器的方法步驟
我們開發(fā)項(xiàng)目常常將項(xiàng)目上傳到linux遠(yuǎn)程服務(wù)器上來運(yùn)行,本文主要介紹了GoLand一鍵上傳項(xiàng)目到遠(yuǎn)程服務(wù)器的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06Go語言超時(shí)退出的三種實(shí)現(xiàn)方式總結(jié)
這篇文章主要為大家詳細(xì)介紹了Go語言中超時(shí)退出的三種實(shí)現(xiàn)方式,文中的示例代碼簡(jiǎn)潔易懂,對(duì)我們深入了解Go語言有一定的幫助,需要的可以了解一下2023-06-06