一篇文章搞懂Go語言中的Context
0 前置知識sync.WaitGroup
sync.WaitGroup是等待一組協(xié)程結束。它實現(xiàn)了一個類似任務隊列的結構,可以向隊列中加入任務,任務完成后就把任務從隊列中移除,如果隊列中的任務沒有全部完成,隊列就會觸發(fā)阻塞以阻止程序繼續(xù)運行。 sync.WaitGroup只有3個方法,Add(),Done(),Wait() 。
其中Done()是Add(-1)的別名,使用Add()添加計數(shù),Done()減掉一個計數(shù),計數(shù)不為0, 阻塞Wait()的運行。
示例:
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)
? }
? //線程結束 -1
? group.Done()
}
func sayHi() {
? //線程結束 -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貌似結束了阻塞...")
}效果:

1 簡介
在 Go 服務器中,每個傳入請求都在其自己的 goroutine 中處理。請求處理程序通常會啟動額外的 goroutine 來訪問后端,例如數(shù)據(jù)庫和 RPC 服務。處理請求的一組 goroutine 通常需要訪問特定于請求的值,例如最終用戶的身份、授權令牌和請求的截止日期。當請求被取消或超時時,處理該請求的所有 goroutine 都應該快速退出,以便系統(tǒng)可以回收它們正在使用的任何資源。
為此,開發(fā)了一個context包,可以輕松地將請求范圍的值、取消信號和截止日期跨 API 邊界傳遞給處理請求所涉及的所有 goroutine。
Context攜帶一個截止日期、一個取消信號和其他跨越API邊界的值。上下文的方法可以被多個gor例程同時調(diào)用。
對服務器的傳入請求應該創(chuàng)建一個上下文,對服務器的傳出調(diào)用應該接受一個上下文。它們之間的函數(shù)調(diào)用鏈必須傳播 Context,可選擇將其替換為使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 創(chuàng)建的派生 Context。當一個上下文被取消時,所有從它派生的上下文也被取消。
WithCancel、WithDeadline 和 WithTimeout 函數(shù)采用 Context(父)并返回派生的 Context(子)和 CancelFunc。調(diào)用 CancelFunc 會取消子項及其子項,刪除父項對子項的引用,并停止任何關聯(lián)的計時器。調(diào)用 CancelFunc 失敗會泄漏子項及其子項,直到父項被取消或計時器觸發(fā)。go vet 工具檢查是否在所有控制流路徑上使用了 CancelFuncs。
使用上下文的程序應遵循以下規(guī)則,以保持跨包的接口一致,并啟用靜態(tài)分析工具來檢查上下文傳播:
不要將上下文存儲在結構類型中;相反,將 Context 顯式傳遞給需要它的每個函數(shù)。
Context 應該是第一個參數(shù),通常命名為 ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... 使用 ctx ...
}即使函數(shù)允許,也不要傳遞 nil 上下文。如果不確定要使用哪個 Context,請傳遞 context.TODO。
僅將上下文值用于傳輸流程和 API 的請求范圍數(shù)據(jù),而不用于將可選參數(shù)傳遞給函數(shù)。
相同的 Context 可以傳遞給在不同的 goroutine 中運行的函數(shù);上下文對于多個 goroutine 同時使用是安全的。

2 context.Context引入
//上下文攜帶截止日期、取消信號和請求范圍的值在API的界限。它的方法是安全的同時使用多個了goroutine。
type Context interface {
? ?// Done返回一個在上下文被取消或超時時關閉的通道。
? ?Done() <-chan struct{}
?
? ?// Err表示在Done通道關閉后為何取消此上下文。
? ?Err() error
?
? ?// Deadline返回上下文將被取消的時間(如果有的話)。
? ?Deadline() (deadline time.Time, ok bool)
?
? ?// Value返回與key相關的值,如果沒有則返回nil。
? ?Value(key interface{}) interface{}
}- 該
Done方法返回一個通道,該通道作為代表運行的函數(shù)的取消信號Context:當通道關閉時,函數(shù)應該放棄它們的工作并返回。 - 該
Err方法返回一個錯誤,指示Context取消的原因。 - 一個
Context對于多個 goroutine 同時使用是安全的。代碼可以將單個傳遞Context給任意數(shù)量的 goroutines 并取消它Context以向所有goroutine 發(fā)出信號。 - 該
Deadline方法允許函數(shù)確定它們是否應該開始工作,還可以使用截止日期來設置 I/O 操作的超時時間。 Value允許一個Context攜帶請求范圍的數(shù)據(jù)。該數(shù)據(jù)必須是安全的,以便多個 goroutine 同時使用。
3 context包的其他常用函數(shù)
3.1 context.Background和context.TODO
Background是任何Context樹的根,它永遠不會被取消:
//Background返回一個空的Context。 它永遠不會取消,沒有截止日期,沒有價值。 Background通常用于main、init和tests,并作為傳入請求的頂級上下文。 ? func Background() Context
給一個函數(shù)方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()
3.2 context.WithCancel和
WithCancelt返回派生的Context值,可以比父Context更快地取消。當請求處理程序返回時,通常會取消與傳入請求關聯(lián)的content。當使用多個副本時,WithCancel對于取消冗余請求也很有用。
// WithCancel返回一個父進程的副本,該父進程的Done通道被盡快關閉。?關閉Done或調(diào)用cancel。 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // CancelFunc取消一個上下文。 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為空時將不會執(zhí)行這個
? ? ? ? case <-ctx.Done():
? ? ? ? ? ?return // return結束該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
? ? }
? }
}擴展:go中select的用法
```
select的用法與switch語言非常類似,由select開始一個新的選擇塊,每個選擇條件由case語句來描述。
與switch語句相比, select有比較多的限制,其中最大的一條限制就是每個case語句里必須是一個IO操作,大致的結構如下:
``` go
select {
? case <-chan1:
? ? ?// 如果chan1成功讀到數(shù)據(jù),則進行該case處理語句
? case chan2 <- 1:
? ? ?// 如果成功向chan2寫入數(shù)據(jù),則進行該case處理語句
? default:
? ? ?// 如果上面都沒有成功,則進入default處理流程
}
```
在一個select語句中,Go語言會按順序從頭至尾評估每一個發(fā)送和接收的語句。
如果其中的任意一語句可以繼續(xù)執(zhí)行(即沒有被阻塞),那么就從那些可以執(zhí)行的語句中任意選擇一條來使用。
如果沒有任意一條語句可以執(zhí)行(即所有的通道都被阻塞),那么有兩種可能的情況:
- 如果給出了default語句,那么就會執(zhí)行default語句,同時程序的執(zhí)行會從select語句后的語句中恢復。
- 如果沒有default語句,那么select語句將被阻塞,直到至少有一個通信可以進行下去。
```3.3 context.WithTimeout
WithTimeout返回派生的Context值,WithTimeout用于設置請求到后端服務器的截止日期:
//WithTimeout返回一個父進程的副本,該父進程的Done通道被立即關閉的父母。關閉“完成”、調(diào)用“取消”或超時結束。新 //Context的Deadline是現(xiàn)在的更快+timeout和父的Deadline,如果任何。?如果計時器仍然在運行,則cancel函數(shù)釋放它資源。 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // CancelFunc取消一個上下文。 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ù)據(jù)庫耗時10毫秒
? ? ?select {
? ? ?case <-ctx.Done(): // 50毫秒后自動調(diào)用
? ? ? ? break LOOP
? ? ?default:
? ? }
? }
? fmt.Println("worker done!")
? wg.Done()
}
func main() {
? // 設置一個50毫秒的超時
? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
? wg.Add(1)
? go worker(ctx)
? time.Sleep(time.Second * 5)
? cancel() // 通知子goroutine結束
? wg.Wait()
? fmt.Println("over")
}
執(zhí)行結果:

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會過期,但在任何情況下調(diào)用它的cancel函數(shù)都是很好的實踐。
? // 如果不這樣做,可能會使上下文及其父類存活的時間超過必要的時間。
? defer cancel()
? select {
? case <-time.After(1 * time.Second):
? ? ?fmt.Println("over")
? case <-ctx.Done():
? ? ?fmt.Println(ctx.Err())
? }
}執(zhí)行結果:

3.5 context.WithValue
WithValue提供了一種將請求范圍的值與Context關聯(lián)的方法 :
//WithValue返回父元素的副本,其Value方法返回val for key。
func WithValue(parent Context, key interface{}, val interface{}) Context了解如何使用context包的最好方法是通過一個已工作的示例。
示例:
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ù)據(jù)庫耗時10毫秒
? ? ?select {
? ? ?case <-ctx.Done(): // 50毫秒后自動調(diào)用
? ? ? ? break LOOP
? ? ?default:
? ? }
? }
? fmt.Println("worker is over!")
? wg.Done()
}
?
func main() {
? // 設置一個50毫秒的超時
? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
? // 在系統(tǒng)的入口中設置trace code傳遞給后續(xù)啟動的goroutine實現(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結束
? wg.Wait()
? fmt.Println("over")
}執(zhí)行結果:

4 實例:請求瀏覽器超時
server端:
package main
import (
? "fmt"
? "math/rand"
? "net/http"
? "time"
)
// server端,隨機出現(xiàn)慢響應
func indexHandler(w http.ResponseWriter, r *http.Request) {
? number := rand.Intn(2)
? if number == 0 {
? ? ?time.Sleep(time.Second * 10) // 耗時10秒的慢響應
? ? ?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) // 使用帶超時的ctx創(chuàng)建一個新的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() {
? // 定義一個100毫秒的超時
? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
? defer cancel() // 調(diào)用cancel釋放子goroutine資源
? doCall(ctx)
}5 Context包都在哪些地方使用
許多服務器框架提供了用于承載請求作用域值的包和類型。我們可以定義“Context”接口的新實現(xiàn),在使用現(xiàn)有框架的代碼和需要“Context”參數(shù)的代碼之間架起橋梁。
6 小結
在谷歌中,要求Go程序員將“Context”參數(shù)作為傳入和傳出請求之間的調(diào)用路徑上的每個函數(shù)的第一個參數(shù)傳遞。這使得許多不同團隊開發(fā)的Go代碼能夠很好地互操作。它提供了對超時和取消的簡單控制,并確保像安全憑證這樣的關鍵值能夠正確地傳輸Go程序。
想要構建在“Context”上的服務器框架應該提供“Context”的實現(xiàn)來連接它們的包和那些需要“Context”參數(shù)的包。然后,它們的客戶端庫將接受來自調(diào)用代碼的“Context”。通過為請求范圍的數(shù)據(jù)和取消建立一個公共接口,“上下文”使包開發(fā)人員更容易共享創(chuàng)建可伸縮服務的代碼。
到此這篇關于一篇文章搞懂Go語言中的Context的文章就介紹到這了,更多相關 Go Context內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
一文教你如何快速學會Go的切片和數(shù)組數(shù)據(jù)類型
數(shù)組是屬于同一類型的元素的集合。切片是數(shù)組頂部的方便、靈活且功能強大的包裝器。本文就來和大家聊聊Go中切片和數(shù)組的使用,需要的可以參考一下2023-03-03

