背景
在go服務(wù)器中,對(duì)于每個(gè)請(qǐng)求的request都是在單獨(dú)的goroutine中進(jìn)行的,處理一個(gè)request也可能設(shè)計(jì)多個(gè)goroutine之間的交互, 使用context可以使開(kāi)發(fā)者方便的在這些goroutine里傳遞request相關(guān)的數(shù)據(jù)、取消goroutine的signal或截止日期。
Context是Golang官方定義的一個(gè)package,它定義了Context類(lèi)型,里面包含了Deadline/Done/Err方法以及綁定到Context上的成員變量值Value,具體定義如下:
Context結(jié)構(gòu)
// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this Context is canceled // or times out. Done() <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }
那么到底什么Context?
可以字面意思可以理解為上下文,比較熟悉的有進(jìn)程/線程上線文,關(guān)于Golang中的上下文,一句話概括就是:goroutine的相關(guān)環(huán)境快照,其中包含函數(shù)調(diào)用以及涉及的相關(guān)的變量值。
通過(guò)Context可以區(qū)分不同的goroutine請(qǐng)求,因?yàn)樵贕olang Severs中,每個(gè)請(qǐng)求都是在單個(gè)goroutine中完成的。
注:關(guān)于goroutine的理解可以移步這里。
由于在Golang severs中,每個(gè)request都是在單個(gè)goroutine中完成,并且在單個(gè)goroutine(不妨稱之為A)中也會(huì)有請(qǐng)求其他服務(wù)(啟動(dòng)另一個(gè)goroutine(稱之為B)去完成)的場(chǎng)景,這就會(huì)涉及多個(gè)Goroutine之間的調(diào)用。如果某一時(shí)刻請(qǐng)求其他服務(wù)被取消或者超時(shí),則作為深陷其中的當(dāng)前goroutine B需要立即退出,然后系統(tǒng)才可回收B所占用的資源。
即一個(gè)request中通常包含多個(gè)goroutine,這些goroutine之間通常會(huì)有交互。

那么,如何有效管理這些goroutine成為一個(gè)問(wèn)題(主要是退出通知和元數(shù)據(jù)傳遞問(wèn)題),Google的解決方法是Context機(jī)制,相互調(diào)用的goroutine之間通過(guò)傳遞context變量保持關(guān)聯(lián),這樣在不用暴露各goroutine內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的前提下,有效地控制各goroutine的運(yùn)行。

如此一來(lái),通過(guò)傳遞Context就可以追蹤goroutine調(diào)用樹(shù),并在這些調(diào)用樹(shù)之間傳遞通知和元數(shù)據(jù)。
雖然goroutine之間是平行的,沒(méi)有繼承關(guān)系,但是Context設(shè)計(jì)成是包含父子關(guān)系的,這樣可以更好的描述goroutine調(diào)用之間的樹(shù)型關(guān)系。
使用:
Done 方法在Context被取消或超時(shí)時(shí)返回一個(gè)close的channel,close的channel可以作為廣播通知,告訴給context相關(guān)的函數(shù)要停止當(dāng)前工作然后返回。
當(dāng)一個(gè)父operation啟動(dòng)一個(gè)goroutine用于子operation,這些子operation不能夠取消父operation。下面描述的WithCancel函數(shù)提供一種方式可以取消新創(chuàng)建的Context.
Context可以安全的被多個(gè)goroutine使用。開(kāi)發(fā)者可以把一個(gè)Context傳遞給任意多個(gè)goroutine然后cancel這個(gè)context的時(shí)候就能夠通知到所有的goroutine。
Err方法返回context為什么被取消。
Deadline返回context何時(shí)會(huì)超時(shí)。
Value返回context相關(guān)的數(shù)據(jù)。
繼承的Context
BackGround(頂層Context:Background)
要?jiǎng)?chuàng)建Context樹(shù),首先就是要?jiǎng)?chuàng)建根節(jié)點(diǎn)
// Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests, // and as the top-level Context for incoming requests. func Background() Context
BackGound是所有Context的root,不能夠被cancel。
該Context通常由接收request的第一個(gè)goroutine創(chuàng)建,它不能被取消、沒(méi)有值、也沒(méi)有過(guò)期時(shí)間,常作為處理request的頂層context存在。
下層Context:WithCancel/WithDeadline/WithTimeout
了根節(jié)點(diǎn)之后,接下來(lái)就是創(chuàng)建子孫節(jié)點(diǎn)。為了可以很好的控制子孫節(jié)點(diǎn),Context包提供的創(chuàng)建方法均是帶有第二返回值(CancelFunc類(lèi)型),它相當(dāng)于一個(gè)Hook,在子goroutine執(zhí)行過(guò)程中,可以通過(guò)觸發(fā)Hook來(lái)達(dá)到控制子goroutine的目的(通常是取消,即讓其停下來(lái))。再配合Context提供的Done方法,子goroutine可以檢查自身是否被父級(jí)節(jié)點(diǎn)Cancel:
select { case <-ctx.Done(): // do some clean… }
注:父節(jié)點(diǎn)Context可以主動(dòng)通過(guò)調(diào)用cancel方法取消子節(jié)點(diǎn)Context,而子節(jié)點(diǎn)Context只能被動(dòng)等待。同時(shí)父節(jié)點(diǎn)Context自身一旦被取消(如其上級(jí)節(jié)點(diǎn)Cancel),其下的所有子節(jié)點(diǎn)Context均會(huì)自動(dòng)被取消。
有三種創(chuàng)建方法:
// 帶cancel返回值的Context,一旦cancel被調(diào)用,即取消該創(chuàng)建的context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // 帶有效期cancel返回值的Context,即必須到達(dá)指定時(shí)間點(diǎn)調(diào)用的cancel方法才會(huì)被執(zhí)行 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) // 帶超時(shí)時(shí)間cancel返回值的Context,類(lèi)似Deadline,前者是時(shí)間點(diǎn),后者為時(shí)間間隔 // 相當(dāng)于WithDeadline(parent, time.Now().Add(timeout)). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithCancel
WithCancel返回一個(gè)繼承的Context,這個(gè)Context在父Context的Done被關(guān)閉時(shí)關(guān)閉自己的Done通道,或者在自己被Cancel的時(shí)候關(guān)閉自己的Done。
WithCancel同時(shí)還返回一個(gè)取消函數(shù)cancel,這個(gè)cancel用于取消當(dāng)前的Context。
package main import ( "context" "log" "os" "time" ) var logg *log.Logger func someHandler() { ctx, cancel := context.WithCancel(context.Background()) go doStuff(ctx) //10秒后取消doStuff time.Sleep(10 * time.Second) cancel() } //每1秒work一下,同時(shí)會(huì)判斷ctx是否被取消了,如果是就退出 func doStuff(ctx context.Context) { for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): logg.Printf("done") return default: logg.Printf("work") } } } func main() { logg = log.New(os.Stdout, "", log.Ltime) someHandler() logg.Printf("down") }
返回:
E:\wdy\goproject>go run context_learn.go 15:06:44 work 15:06:45 work 15:06:46 work 15:06:47 work 15:06:48 work 15:06:49 work 15:06:50 work 15:06:51 work 15:06:52 work 15:06:53 down
package main import ( "context" "fmt" "time" ) func someHandler() { // 創(chuàng)建繼承Background的子節(jié)點(diǎn)Context ctx, cancel := context.WithCancel(context.Background()) go doSth(ctx) //模擬程序運(yùn)行 - Sleep 5秒 time.Sleep(5 * time.Second) cancel() } //每1秒work一下,同時(shí)會(huì)判斷ctx是否被取消,如果是就退出 func doSth(ctx context.Context) { var i = 1 for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println("done") return default: fmt.Printf("work %d seconds: \n", i) } i++ } } func main() { fmt.Println("start...") someHandler() fmt.Println("end.") }
輸出結(jié)果:

withDeadline withTimeout
WithTimeout func(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
WithTimeout 等價(jià)于 WithDeadline(parent, time.Now().Add(timeout)).
對(duì)上面的樣例代碼進(jìn)行修改
func timeoutHandler() { // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) // go doTimeOutStuff(ctx) go doStuff(ctx) time.Sleep(10 * time.Second) cancel() } func main() { logg = log.New(os.Stdout, "", log.Ltime) timeoutHandler() logg.Printf("end") }
返回:
15:59:22 work 15:59:24 work 15:59:25 work 15:59:26 work 15:59:27 done 15:59:31 end
可以看到doStuff在context超時(shí)的時(shí)候被取消了,ctx.Done()被關(guān)閉。
將context.WithDeadline替換為context.WithTimeout
func timeoutHandler() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) // go doTimeOutStuff(ctx) go doStuff(ctx) time.Sleep(10 * time.Second) cancel() }
16:02:47 work 16:02:49 work 16:02:50 work 16:02:51 work 16:02:52 done 16:02:56 end
doTimeOutStuff替換doStuff
func doTimeOutStuff(ctx context.Context) { for { time.Sleep(1 * time.Second) if deadline, ok := ctx.Deadline(); ok { //設(shè)置了deadl logg.Printf("deadline set") if time.Now().After(deadline) { logg.Printf(ctx.Err().Error()) return } } select { case <-ctx.Done(): logg.Printf("done") return default: logg.Printf("work") } } } func timeoutHandler() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) go doTimeOutStuff(ctx) // go doStuff(ctx) time.Sleep(10 * time.Second) cancel() }
16:03:55 deadline set 16:03:55 work 16:03:56 deadline set 16:03:56 work 16:03:57 deadline set 16:03:57 work 16:03:58 deadline set 16:03:58 work 16:03:59 deadline set 16:03:59 context deadline exceeded 16:04:04 end
WithTimeout
package main import ( "math/rand" "time" "sync" "fmt" "context" ) func main() { rand.Seed(time.Now().Unix()) ctx,_:=context.WithTimeout(context.Background(),time.Second*3) var wg sync.WaitGroup wg.Add(1) go GenUsers(ctx,&wg) wg.Wait() fmt.Println("生成幸運(yùn)用戶成功") } func GenUsers(ctx context.Context,wg *sync.WaitGroup) { //生成用戶ID fmt.Println("開(kāi)始生成幸運(yùn)用戶") users:=make([]int,0) guser:for{ select{ case <- ctx.Done(): //代表父context發(fā)起 取消操作 fmt.Println(users) wg.Done() break guser return default: users=append(users,getUserID(1000,100000)) } } } func getUserID(min int ,max int) int { return rand.Intn(max-min)+min }
context deadline exceeded就是ctx超時(shí)的時(shí)候ctx.Err的錯(cuò)誤消息。
搜索測(cè)試程序
完整代碼參見(jiàn)官方文檔Go Concurrency Patterns: Context,其中關(guān)鍵的地方在于函數(shù)httpDo
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { // Run the HTTP request in a goroutine and pass the response to f. tr := &http.Transport{} client := &http.Client{Transport: tr} c := make(chan error, 1) go func() { c <- f(client.Do(req)) }() select { case <-ctx.Done(): tr.CancelRequest(req) <-c // Wait for f to return. return ctx.Err() case err := <-c: return err } }
httpDo關(guān)鍵的地方在于
select { case <-ctx.Done(): tr.CancelRequest(req) <-c // Wait for f to return. return ctx.Err() case err := <-c: return err }
要么ctx被取消,要么request請(qǐng)求出錯(cuò)。
httpserver中實(shí)現(xiàn)超時(shí)
package main import ( "net/http" "context" "time" ) func CountData(c chan string) chan string { time.Sleep(time.Second*5) c<- "統(tǒng)計(jì)結(jié)果" return c } type IndexHandler struct {} func(this *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("count")==""{ w.Write([]byte("這是首頁(yè)")) }else { ctx,cancel:=context.WithTimeout(r.Context(),time.Second*3) defer cancel() c:=make(chan string) go CountData(c) select { case <-ctx.Done(): w.Write([]byte("超時(shí)")) case ret:=<-c: w.Write([]byte(ret)) } } } func main() { mux:=http.NewServeMux() mux.Handle("/",new(IndexHandler)) http.ListenAndServe(":8082",mux) }
超時(shí)場(chǎng)景:
package main import ( "context" "fmt" "time" ) func timeoutHandler() { // 創(chuàng)建繼承Background的子節(jié)點(diǎn)Context ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) go doSth(ctx) //模擬程序運(yùn)行 - Sleep 10秒 time.Sleep(10 * time.Second) cancel() // 3秒后將提前取消 doSth goroutine } //每1秒work一下,同時(shí)會(huì)判斷ctx是否被取消,如果是就退出 func doSth(ctx context.Context) { var i = 1 for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println("done") return default: fmt.Printf("work %d seconds: \n", i) } i++ } } func main() { fmt.Println("start...") timeoutHandler() fmt.Println("end.") }

WithValue
func WithValue(parent Context, key interface{}, val interface{}) Context
// NewContext returns a new Context carrying userIP. func NewContext(ctx context.Context, userIP net.IP) context.Context { return context.WithValue(ctx, userIPKey, userIP) } // FromContext extracts the user IP address from ctx, if present. func FromContext(ctx context.Context) (net.IP, bool) { // ctx.Value returns nil if ctx has no value for the key; // the net.IP type assertion returns ok=false for nil. userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok }
前面鋪地了這么多。
確實(shí),通過(guò)引入Context包,一個(gè)request范圍內(nèi)所有g(shù)oroutine運(yùn)行時(shí)的取消可以得到有效的控制。但是這種解決方式卻不夠優(yōu)雅。
一旦代碼中某處用到了Context,傳遞Context變量(通常作為函數(shù)的第一個(gè)參數(shù))會(huì)像病毒一樣蔓延在各處調(diào)用它的地方。比如在一個(gè)request中實(shí)現(xiàn)數(shù)據(jù)庫(kù)事務(wù)或者分布式日志記錄,創(chuàng)建的context,會(huì)作為參數(shù)傳遞到任何有數(shù)據(jù)庫(kù)操作或日志記錄需求的函數(shù)代碼處。即每一個(gè)相關(guān)函數(shù)都必須增加一個(gè)context.Context類(lèi)型的參數(shù),且作為第一個(gè)參數(shù),這對(duì)無(wú)關(guān)代碼完全是侵入式的。
更多詳細(xì)內(nèi)容可參見(jiàn):Michal Strba 的context-should-go-away-go2文章
Google Group上的討論可移步這里。
Context機(jī)制最核心的功能是在goroutine之間傳遞cancel信號(hào),但是它的實(shí)現(xiàn)是不完全的。
Cancel可以細(xì)分為主動(dòng)與被動(dòng)兩種,通過(guò)傳遞context參數(shù),讓調(diào)用goroutine可以主動(dòng)cancel被調(diào)用goroutine。但是如何得知被調(diào)用goroutine什么時(shí)候執(zhí)行完畢,這部分Context機(jī)制是沒(méi)有實(shí)現(xiàn)的。而現(xiàn)實(shí)中的確又有一些這樣的場(chǎng)景,比如一個(gè)組裝數(shù)據(jù)的goroutine必須等待其他goroutine完成才可開(kāi)始執(zhí)行,這是context明顯不夠用了,必須借助sync.WaitGroup。
func serve(l net.Listener) error { var wg sync.WaitGroup var conn net.Conn var err error for { conn, err = l.Accept() if err != nil { break } wg.Add(1) go func(c net.Conn) { defer wg.Done() handle(c) }(conn) } wg.Wait() return err }
本文來(lái)自博客園,作者:孫龍-程序員,轉(zhuǎn)載請(qǐng)注明原文鏈接:http://www.rzrgm.cn/sunlong88/p/11272559.html
浙公網(wǎng)安備 33010602011771號(hào)