context是Go語言官方定義的一個包,稱之爲上下文。html
Go中的context包在與API和慢進程交互時能夠派上用場,特別是在提供Web請求的生產級系統中。在哪裏,您可能想要通知全部goroutines中止工做並返回。git
這是一個基本教程,介紹如何在項目中使用它以及一些最佳實踐和陷阱。程序員
在瞭解上下文以前,請先了解如下概念github
在Go語言中 context 包容許您傳遞一個 "context" 到您的程序,如超時或截止日期(deadline)或通道(channel),以及指示中止運行和返回等。例如,若是您正在執行Web請求或運行系統命令,那麼對生產級系統進行超時控制一般是個好主意。由於,若是您依賴的API運行緩慢,您不但願在系統上備份請求,這可能最終會增長負載並下降您所服務的全部請求的性能。致使級聯效應。這是超時或截止日期context能夠派上用場的地方。golang
這裏咱們先來分析context源碼( https://golang.org/src/contex...)。安全
context
包的核心就是Context
接口,其定義以下:服務器
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
這個接口共有4個方法:app
Done
方法返回一個只讀的chan,類型爲struct{}
,咱們在goroutine中,若是該方法返回的chan能夠讀取,則意味着parent context已經發起了取消請求,咱們經過Done
方法收到這個信號後,就應該作清理操做,而後退出goroutine,釋放資源。Err
方法返回取消的錯誤緣由,由於什麼Context被取消。Value
方法獲取該Context上綁定的值,是一個鍵值對,因此要經過一個Key才能夠獲取對應的值,這個值通常是線程安全的。但使用這些數據的時候要注意同步,好比返回了一個map,而這個map的讀寫則要加鎖。以上四個方法中經常使用的就是Done
了,若是Context取消的時候,咱們就能夠獲得一個關閉的chan,關閉的chan是能夠讀取的,因此只要能夠讀取的時候,就意味着收到Context取消的信號了,如下是這個方法的經典用法。dom
func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } }
Context接口並不須要咱們實現,Go內置已經幫咱們實現了2個(Background、TODO),咱們代碼中最開始都是以這兩個內置的做爲最頂層的partent context(即根context),衍生出更多的子Context。函數
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
Background:主要用於main函數、初始化以及測試代碼中,做爲Context這個樹結構的最頂層的Context,也就是根Context。
TODO:在還不肯定使用context的場景,可能當前函數之後會更新以便使用 context。
這兩個函數的本質是emptyCtx結構體類型。
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
這就是emptyCtx
實現Context接口的方法,能夠看到,這些方法什麼都沒作,返回的都是nil或者零值。
有上面的根context,那麼是如何衍生更多的子Context的呢?這就要靠context包爲咱們提供的With
系列的函數了。
一、取消函數
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
此函數接收一個parent Context參數,父 context 能夠是後臺 context 或傳遞給函數的 context。
返回派生 context 和取消函數。只有建立它的函數才能調用取消函數來取消此 context。若是您願意,能夠傳遞取消函數,可是,強烈建議不要這樣作。這可能致使取消函數的調用者沒有意識到取消 context 的下游影響。可能存在源自此的其餘 context,這可能致使程序以意外的方式運行。簡而言之,永遠不要傳遞取消函數。
示例
package main import ( "fmt" "time" "golang.org/x/net/context" ) func main() { //建立一個可取消子context,context.Background():返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。 ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { //使用select調用<-ctx.Done()判斷是否要結束 case <-ctx.Done(): fmt.Println("goroutine exit") return default: fmt.Println("goroutine running.") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("main fun exit") //取消context cancel() time.Sleep(5 * time.Second) }
二、超時控制
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):
此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您能夠建立一個將在之後的某個時間自動取消的 context,並在子函數中傳遞它。當由於截止日期耗盡而取消該 context 時,獲此 context 的全部函數都會收到通知去中止運行並返回。
示例
package main import ( "fmt" "golang.org/x/net/context" "time" ) func main() { d := time.Now().Add(2 * time.Second) //設置超時控制WithDeadline,超時時間2 ctx, cancel := context.WithDeadline(context.Background(), d) defer cancel() select { case <-time.After(3 * time.Second): fmt.Println("timeout") case <-ctx.Done(): //2到了到了,執行該代碼 fmt.Println(ctx.Err()) } }
三、超時控制
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):
此函數相似於 context.WithDeadline。不一樣之處在於它將持續時間做爲參數輸入而不是時間對象。此函數返回派生 context,若是調用取消函數或超出超時持續時間,則會取消該派生 context。
package main import ( "fmt" "golang.org/x/net/context" "time" ) func main() { //設置超時控制WithDeadline,超時時間2 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() select { case <-time.After(3 * time.Second): fmt.Println("timeout") case <-ctx.Done(): //2到了到了,執行該代碼 fmt.Println(ctx.Err()) } }
四、返回派生的context
func WithValue(parent Context, key, val interface{}) Context:
此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並經過 context 樹與 context 一塊兒傳遞。這意味着一旦得到帶有值的 context,從中派生的任何 context 都會得到此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。
示例
package main import ( "context" "fmt" ) func Route(ctx context.Context) { ret, ok := ctx.Value("id").(int) if !ok { ret = 1 } fmt.Printf("id:%d\n", ret) s, _ := ctx.Value("name").(string) fmt.Printf("name:%s\n", s) } func main() { ctx := context.WithValue(context.Background(), "id", 123) ctx = context.WithValue(ctx, "name", "jerry") Route(ctx) }
在下面的示例中,您能夠看到接受context的函數啓動goroutine並等待返回該goroutine或取消該context。select語句幫助咱們選擇先發生的任何狀況並返回。
<-ctx.Done()
關閉「完成」通道後,將case <-ctx.Done():
選中該通道。一旦發生這種狀況,該功能應該放棄工做並準備返回。這意味着您應該關閉全部打開的管道,釋放資源並從函數返回。有些狀況下,釋放資源能夠阻止返回,好比作一些掛起的清理等等。在處理context返回時,你應該注意任何這樣的可能性。
本節後面的示例有一個完整的go程序,它說明了超時和取消功能。
//Function that does slow processing with a context //Note that context is the first argument func sleepRandomContext(ctx context.Context, ch chan bool) { //Cleanup tasks //There are no contexts being created here //Hence, no canceling needed defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //Make a channel sleeptimeChan := make(chan int) //Start slow processing in a goroutine //Send a channel for communication go sleepRandom("sleepRandomContext", sleeptimeChan) //Use a select statement to exit out if context expires select { 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 synchronization fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //This case is selected when processing finishes before the context is cancelled fmt.Println("Slept for ", sleeptime, "ms") } }
到目前爲止,咱們已經看到使用 context 能夠設置截止日期,超時或調用取消函數來通知全部使用任何派生 context 的函數來中止運行並返回。如下是它如何工做的示例:
main 函數
doWorkContext 函數
這個 context 將被取消當
sleepRandomContext 函數
sleepRandom 函數
Playground: https://play.golang.org/p/grQ... (看起來我使用的隨機種子,在 playground 時間沒有真正改變,您須要在你本機執行去看隨機性)
Github: https://github.com/pagnihotry...
package main import ( "context" "fmt" "math/rand" "time" ) //Slow function func sleepRandom(fromFunction string, ch chan int) { //defer cleanup defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //Perform a slow task //For illustration purpose, //Sleep here for random ms seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.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 in if ch != nil { ch <- sleeptime } } //Function that does slow processing with a context //Note that context is the first argument func sleepRandomContext(ctx context.Context, ch chan bool) { //Cleanup tasks //There are no contexts being created here //Hence, no canceling needed defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //Make a channel sleeptimeChan := make(chan int) //Start slow processing in a goroutine //Send a channel for communication go sleepRandom("sleepRandomContext", sleeptimeChan) //Use a select statement to exit out if context expires select { 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 synchronization fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //This case is selected when processing finishes before the context is cancelled fmt.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 main func 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 ms ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //Cancel to release resources once the function is complete defer 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 channel ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //Use a select statement to exit out if context expires select { 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 cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //This case is selected when processing finishes before the context is cancelled fmt.Println("sleepRandomContext returned") } } func main() { //Make a background context ctx := context.Background() //Derive a context with cancel ctxWithCancel, cancelFunction := context.WithCancel(ctx) //defer canceling so that all the resources are freed up //For this and the derived contexts defer 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 return go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //Do work doWorkContext(ctxWithCancel) }
cmd.Wait()
外部命令的全部分支都已完成處理。若是您使用超時或最後執行時間的最後期限,您可能會發現這不能按預期工做。若是遇到任何此類問題,可使用執行超時time.After
。這就意味着若是您正在編寫一個具備可能須要大量時間的函數的庫,而且您的庫可能會被服務器應用程序使用,那麼您必須接受這些函數中的context。固然,我能夠context.TODO()
隨處經過,但這形成程序可讀性差,程序看起來不夠優雅。
Context
struct沒有cancel方法,由於只有派生context的函數才能取消它。http://p.agnihotry.com/post/u...
https://www.flysnow.org/2017/...
https://faiface.github.io/pos...