![](https://raw.githubusercontent.com/studygolang/gctt-images/master/understanding-the-context-package-in-golang/0_exTPQ4ppfrdjuXcR.jpg)Go 中的 context 包在與 API 和慢處理互動時可以派上用場,特別是在生產級的 Web 服務中。在這些情境中,您可能想要通知所有的 goroutine 停止運行並返回。這是一個基本教程,介紹如何在項目中使用它以及一些最佳實務和陷阱。要理解 context 包,您應該熟悉兩個概念。在轉到 context 之前,我將簡要介紹這些內容,如果您已經熟悉,則可以直接轉到 context 部分。## Goroutine來自 Go 語言官方文檔:"goroutine 是一個輕量級的執行線程"。多個 goroutine 比一個線程輕量所以管理它們消耗的資源相對更少。Playground: https://play.golang.org/p/-TDMgnkJRY6```gopackage mainimport "fmt"//function to print hellofunc printHello() {fmt.Println("Hello from printHello")}func main() {//inline goroutine. Define a function inline and then call it.go func(){fmt.Println("Hello inline")}()//call a function as goroutinego printHello()fmt.Println("Hello from main")}```如果您運行上面的程式,您只能看到 main 中列印的 Hello, 因為它啟動了兩個 goroutine 並在它們完成前退出了。為了讓 main 等待這些 goroutine 執行完,您需要一些方法讓這些 goroutine 告訴 main 它們執行完了,那就需要用到通道。## 通道(channel)這是 goroutine 之間的溝通渠道。當您想要將結果或錯誤,或任何其他類型的資訊從一個 goroutine 傳遞到另一個 goroutine 時就可以使用通道。通道是有類型的,可以是 int 類型的通道接收整數或錯誤類型的接收錯誤等。假設有個 int 類型的通道 ch,如果你想發一些資訊到這個通道,文法是 ch <- 1,如果你想從這個通道接收一些資訊,文法就是 var := <-ch。這將從這個通道接收並儲存值到 var 變數。以下程式說明了通道的使用確保了 goroutine 執行完成並將值返回給 main 。注意:WaitGroup( https://golang.org/pkg/sync/#WaitGroup )也可用於同步,但稍後在 context 部分我們談及通道,所以在這篇部落格中的範例程式碼,我選擇了它們。Playground: https://play.golang.org/p/3zfQMox5mHn```gopackage mainimport "fmt"//prints to stdout and puts an int on channelfunc printHello(ch chan int) {fmt.Println("Hello from printHello")//send a value on channelch <- 2}func main() {//make a channel. You need to use the make function to create channels.//channels can also be buffered where you can specify size. eg: ch := make(chan int, 2)//that is out of the scope of this post.ch := make(chan int)//inline goroutine. Define a function and then call it.//write on a channel when donego func() {fmt.Println("Hello inline")//send a value on channelch <- 1}()//call a function as goroutinego printHello(ch)fmt.Println("Hello from main")//get first value from channel.//and assign to a variable to use this value later//here that is to print iti := <-chfmt.Println("Recieved ", i)//get the second value from channel//do not assign it to a variable because we dont want to use that<-ch}```在 Go 語言中 context 包允許您傳遞一個 "context" 到您的程式。 Context 如逾時或到期日(deadline)或通道,來指示停止運行和返回。例如,如果您正在執行一個 web 請求或運行一個系統命令,定義一個逾時對生產級系統通常是個好主意。因為,如果您依賴的API運行緩慢,你不希望在系統上備份(back up)請求,因為它可能最終會增加負載並降低所有請求的執行效率。導致級聯效應。這是逾時或到期日 context 派上用場的地方。## 建立 contextcontext 包允許以下方式建立和獲得 context:### context.Background() Context這個函數返回一個空 context。這隻能用於高等級(在 main 或頂級請求處理中)。這能用於派生我們稍後談及的其他 context 。```goctx := context.Background()```### context.TODO() Context這個函數也是建立一個空 context。也只能用於高等級或當您不確定使用什麼 context,或函數以後會更新以便接收一個 context 。這意味您(或維護者)計劃將來要添加 context 到函數。```goctx := context.TODO()```有趣的是,[查看代碼](https://golang.org/src/context/context.go),它與 background 完全相同。不同的是,靜態分析工具可以使用它來驗證 context 是否正確傳遞,這是一個重要的細節,因為靜態分析工具可以協助在早期發現潛在的錯誤,並且可以串連到 CI/CD 管道。來自 https://golang.org/src/context/context.go:```govar (background = new(emptyCtx)todo = new(emptyCtx))```### context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並通過 context 樹與 context 一起傳遞。這意味著一旦獲得帶有值的 context,從中派生的任何 context 都會獲得此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。```goctx := context.WithValue(context.Background(), key, "test")```### context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)這是它開始變得有趣的地方。此函數建立從傳入的父 context 派生的新 context。父 context 可以是後台 context 或傳遞給函數的 context。返回派生 context 和取消函數。只有建立它的函數才能調用取消函數來取消此 context。如果您願意,可以傳遞取消函數,但是,強烈建議不要這樣做。這可能導致取消函數的調用者沒有意識到取消 context 的下遊影響。可能存在源自此的其他 context,這可能導致程式以意外的方式運行。簡而言之,永遠不要傳遞取消函數。```goctx, cancel := context.WithCancel(context.Background())```### context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)此函數返回其父項的派生 context,當到期日超過或取消函數被調用時,該 context 將被取消。例如,您可以建立一個將在以後的某個時間自動取消的 context,並在子函數中傳遞它。當因為到期日耗盡而取消該 context 時,獲此 context 的所有函數都會收到通知去停止運行並返回。```goctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))```### context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)此函數類似於 context.WithDeadline。不同之處在於它將期間作為參數輸入而不是時間對象。此函數返回派生 context,如果調用取消函數或超出逾時期間,則會取消該派生 context。```goctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)```## 函數接收和使用 Context現在我們知道了如何建立 context(Background 和 TODO)以及如何派生 context(WithValue,WithCancel,Deadline 和 Timeout),讓我們討論如何使用它們。在下面的樣本中,您可以看到接受 context 的函數啟動一個 goroutine 並等待 該 goroutine 返回或該 context 取消。select 語句協助我們選擇先發生的任何情況並返回。`<-ctx.Done()` 一旦 Done 通道被關閉,這個 `<-ctx.Done():` 被選擇。一旦發生這種情況,此函數應該放棄運行並準備返回。這意味著您應該關閉所有開啟的管道,釋放資源並從函數返回。有些情況下,釋放資源可以阻止返回,比如做一些掛起的清理等等。在處理 context 返回時,您應該注意任何這樣的可能性。本節後面的樣本有一個完整的 Go 語言程式,它說明了逾時和取消功能。```go//Function that does slow processing with a context//Note that context is the first argumentfunc sleepRandomContext(ctx context.Context, ch chan bool) {//Cleanup tasks//There are no contexts being created here//Hence, no canceling neededdefer func() {fmt.Println("sleepRandomContext complete")ch <- true}()//Make a channelsleeptimeChan := make(chan int)//Start slow processing in a goroutine//Send a channel for communicationgo sleepRandom("sleepRandomContext", sleeptimeChan)//Use a select statement to exit out if context expiresselect {case <-ctx.Done()://If context expires, this case is selected//Free up resources that may no longer be needed because of aborting the work//Signal all the goroutines that should stop work (use channels)//Usually, you would send something on channel,//wait for goroutines to exit and then return//Or, use wait groups instead of channels for synchronizationfmt.Println("Time to return")case sleeptime := <-sleeptimeChan://This case is selected when processing finishes before the context is cancelledfmt.Println("Slept for ", sleeptime, "ms")}}```## 例子到目前為止,我們已經看到使用 context 可以設定到期日,逾時或調用取消函數來通知所有使用任何派生 context 的函數來停止運行並返回。以下是它如何工作的樣本:***main*** 函數* 用 cancel 建立一個 context* 隨機逾時後調用取消函數***doWorkContext*** 函數* 派生一個逾時 context* 這個 context 將被取消當* main 調用取消函數或* 逾時到或* doWorkContext 調用它的取消函數* 啟動 goroutine 傳入派生上下文執行一些慢處理* 等待 goroutine 完成或上下文被 main goroutine 取消,以優先發生者為準***sleepRandomContext*** 函數* 開啟一個 goroutine 去做些緩慢的處理* 等待該 goroutine 完成或,* 等待 context 被 main goroutine 取消,操時或它自己的取消函數被調用***sleepRandom*** 函數* 隨機時間休眠* 此樣本使用休眠來類比隨機處理時間,在實際樣本中,您可以使用通道來通知此函數,以開始清理並在通道上等待它,以確認清理已完成。Playground: https://play.golang.org/p/grQAUN3MBlg (看起來我使用的隨機種子,在 playground 時間沒有真正改變,您需要在你本機執行去看隨機性)Github: https://github.com/pagnihotry/golang_samples/blob/master/go_context_sample.go```gopackage mainimport ("context""fmt""math/rand""time")//Slow functionfunc sleepRandom(fromFunction string, ch chan int) {//defer cleanupdefer func() { fmt.Println(fromFunction, "sleepRandom complete") }()//Perform a slow task//For illustration purpose,//Sleep here for random msseed := time.Now().UnixNano()r := rand.New(rand.NewSource(seed))randomNumber := r.Intn(100)sleeptime := randomNumber + 100fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms")time.Sleep(time.Duration(sleeptime) * time.Millisecond)fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms")//write on the channel if it was passed inif ch != nil {ch <- sleeptime}}//Function that does slow processing with a context//Note that context is the first argumentfunc sleepRandomContext(ctx context.Context, ch chan bool) {//Cleanup tasks//There are no contexts being created here//Hence, no canceling neededdefer func() {fmt.Println("sleepRandomContext complete")ch <- true}()//Make a channelsleeptimeChan := make(chan int)//Start slow processing in a goroutine//Send a channel for communicationgo sleepRandom("sleepRandomContext", sleeptimeChan)//Use a select statement to exit out if context expiresselect {case <-ctx.Done()://If context is cancelled, this case is selected//This can happen if the timeout doWorkContext expires or//doWorkContext calls cancelFunction or main calls cancelFunction//Free up resources that may no longer be needed because of aborting the work//Signal all the goroutines that should stop work (use channels)//Usually, you would send something on channel,//wait for goroutines to exit and then return//Or, use wait groups instead of channels for synchronizationfmt.Println("sleepRandomContext: Time to return")case sleeptime := <-sleeptimeChan://This case is selected when processing finishes before the context is cancelledfmt.Println("Slept for ", sleeptime, "ms")}}//A helper function, this can, in the real world do various things.//In this example, it is just calling one function.//Here, this could have just lived in mainfunc doWorkContext(ctx context.Context) {//Derive a timeout context from context with cancel//Timeout in 150 ms//All the contexts derived from this will returns in 150 msctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond)//Cancel to release resources once the function is completedefer func() {fmt.Println("doWorkContext complete")cancelFunction()}()//Make channel and call context function//Can use wait groups as well for this particular case//As we do not use the return value sent on channelch := make(chan bool)go sleepRandomContext(ctxWithTimeout, ch)//Use a select statement to exit out if context expiresselect {case <-ctx.Done()://This case is selected when the passed in context notifies to stop work//In this example, it will be notified when main calls cancelFunctionfmt.Println("doWorkContext: Time to return")case <-ch://This case is selected when processing finishes before the context is cancelledfmt.Println("sleepRandomContext returned")}}func main() {//Make a background contextctx := context.Background()//Derive a context with cancelctxWithCancel, cancelFunction := context.WithCancel(ctx)//defer canceling so that all the resources are freed up//For this and the derived contextsdefer func() {fmt.Println("Main Defer: canceling context")cancelFunction()}()//Cancel context after a random time//This cancels the request after a random timeout//If this happens, all the contexts derived from this should returngo func() {sleepRandom("Main", nil)cancelFunction()fmt.Println("Main Sleep complete. canceling context")}()//Do workdoWorkContext(ctxWithCancel)}```## 缺陷如果函數接收 context 參數,確保檢查它是如何處理取消通知的。例如,exec.CommandContext 不會關閉讀取管道,直到命令執行了進程建立的所有分支(Github 問題:https://github.com/golang/go/issues/23019 ),這意味著如果等待 cmd.Wait() 直到外部命令的所有分支都已完成,則 context 取消不會使該函數立即返回。如果您使用逾時或到期日,您可能會發現這不能按預期運行。如果遇到任何此類問題,可以使用 time.After 實現逾時。## 最佳實務1. context.Background 只應用在最高等級,作為所有派生 context 的根。2. context.TODO 應用在不確定要使用什麼的地方,或者當前函數以後會更新以便使用 context。3. context 取消是建議性的,這些函數可能需要一些時間來清理和退出。4. context.Value 應該很少使用,它不應該被用來傳遞選擇性參數。這使得 API 隱式的並且可以引起錯誤。取而代之的是,這些值應該作為參數傳遞。5. 不要將 context 儲存在結構中,在函數中顯式傳遞它們,最好是作為第一個參數。6. 永遠不要傳遞不存在的 context 。相反,如果您不確定使用什麼,使用一個 ToDo context。7. Context 結構沒有取消方法,因為只有派生 context 的函數才應該取消 context。
via: https://medium.com/@parikshit/understanding-the-context-package-in-golang-b1392c821d14
作者:Parikshit Agnihotry 譯者:themoonbear 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
306 次點擊