1. 程式人生 > >Go語言的context包從放棄到入門

Go語言的context包從放棄到入門

[toc] 以下內容由偉大的詩人、哲學家chenqionghe吐血傳授,希望能幫助到你,謝謝~ # 一、Context包到底是幹嘛用的 我們會在用到很多東西的時候都看到context的影子,比如gin框架,比如grpc,這東西到底是做啥的? 大家都在用,沒幾個知道這是幹嘛的,知其然而不知其所以然 >誰都在CRUD,誰都覺得if else就完了,有程式碼能copy我也行,原理啥啥不懂不重要,反正就是一把梭 原理說白了就是: 1. 當前協程取消了,可以通知所有由它建立的子協程退出 2. 當前協程取消了,不會影響到建立它的父級協程的狀態 3. 擴充套件了額外的功能:超時取消、定時取消、可以給子協程共享資料 # 二、主協程退出通知子協程示例演示 ## 主協程通知子協程退出 如下程式碼展示了,通過一個叫done的channel通道達到了這樣的效果 ``` package main import ( "fmt" "time" ) func main() { done := make(chan string) //緩衝通道預先放置10個訊息 messages := make(chan int, 10) defer close(messages) for i := 0; i < 10; i++ { messages <- i } //啟動3個子協程消費messages訊息 for i := 1; i <= 3; i++ { go child(i, done, messages) } time.Sleep(3 * time.Second) //等待子協程接收一半的訊息 close(done) //結束前通知子協程 time.Sleep(2 * time.Second) //等待所有的子協程輸出 fmt.Println("主協程結束") } //從messages通道獲取資訊,當收到結束訊號的時候不再接收 func child(i int, done <-chan string, messages <-chan int) { Consume: for { time.Sleep(1 * time.Second) select { case <-done: fmt.Printf("[%d]被主協程通知結束...\n", i) break Consume default: fmt.Printf("[%d]接收訊息: %d\n", i, <-messages) } } } ``` 執行結束如下 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115235284-1236735018.png) 這裡,我們用一個channel的關閉做到了通知所有的消費到一半的子協程退出。 問題來了,如果子協程又要啟動它的子協程,這可咋整? ## 主協程通知有子協程,子協程又有多個子協程 這是可哲學問題,我們還是得建立一個叫done的channel來監測 下面演示一下這種操作,再在每個child方法裡啟動多個job,如下 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115254639-1747491985.png) 全量程式碼貼出來 ``` package main import ( "fmt" "time" ) func main() { done := make(chan string) //緩衝通道預先放置10個訊息 messages := make(chan int, 10) defer close(messages) for i := 0; i < 10; i++ { messages <- i } //啟動3個子協程消費messages訊息 for i := 1; i <= 3; i++ { go child(i, done, messages) } time.Sleep(3 * time.Second) //等待子協程接收一半的訊息 close(done) //結束前通知子協程 time.Sleep(2 * time.Second) //等待所有的子協程輸出 fmt.Println("主協程結束") } //從messages通道獲取資訊,當收到結束訊號的時候不再接收 func child(i int, done <-chan string, messages <-chan int) { newDone := make(chan string) defer close(newDone) go childJob(i, "a", newDone) go childJob(i, "b", newDone) Consume: for { time.Sleep(1 * time.Second) select { case <-done: fmt.Printf("[%d]被主協程通知結束...\n", i) break Consume default: fmt.Printf("[%d]接收訊息: %d\n", i, <-messages) } } } //任務 func childJob(parent int, name string, done <-chan string) { for { time.Sleep(1 * time.Second) select { case <-done: fmt.Printf("[%d-%v]被結束...\n", parent, name) return default: fmt.Printf("[%d-%v]執行\n", parent, name) } } } ``` 執行結果如下 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115304469-1984647477.png) 問題來了,如果job裡再啟動自己的goroutine,這樣沒完沒了的建立done的通道有點噁心,這時候context包就來了! 我們先把上面的程式碼改成context包的方式 ``` package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) //緩衝通道預先放置10個訊息 messages := make(chan int, 10) defer close(messages) for i := 0; i < 10; i++ { messages <- i } //啟動3個子協程消費messages訊息 for i := 1; i <= 3; i++ { go child(i, ctx, messages) } time.Sleep(3 * time.Second) //等待子協程接收一半的訊息 cancel() //結束前通知子協程 time.Sleep(2 * time.Second) //等待所有的子協程輸出 fmt.Println("主協程結束") } //從messages通道獲取資訊,當收到結束訊號的時候不再接收 func child(i int, ctx context.Context, messages <-chan int) { //基於父級的context建立context newCtx, _ := context.WithCancel(ctx) go childJob(i, "a", newCtx) go childJob(i, "b", newCtx) Consume: for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Printf("[%d]被主協程通知結束...\n", i) break Consume default: fmt.Printf("[%d]接收訊息: %d\n", i, <-messages) } } } //任務 func childJob(parent int, name string, ctx context.Context) { for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Printf("[%d-%v]被結束...\n", parent, name) return default: fmt.Printf("[%d-%v]執行\n", parent, name) } } } ``` 執行結果如下 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115320363-1653332763.png) 可以看到,改成context包還是順利的通過子協程退出了 主要修改了幾個地方,再ctx向下傳遞 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115326378-1520109390.png) 基於上層context再構建當前層級的context ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115339876-827776852.png) 監聽context的退出訊號, ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209115343705-1389596842.png) 這就是context包的核心原理,鏈式傳遞context,基於context構造新的context # 三、Context包的核心介面和方法 更多資料可以檢視:[Go 語言設計與實現](https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/) ## context介面 context是一個介面,主要包含以下4個方法 * Deadline 返回當前context任務被取消的時間,沒有設定返回ok返回false * Done 當綁定當前的context任務被取消時,將返回一個關閉的channel * Err Done返回的channel沒有關閉,返回nil; Done返回的channel已經關閉,返回非空值表示任務結束的原因; context被取消,返回Canceled。 context超時,DeadlineExceeded * Value 返回context儲存的鍵 ## emptyCtx結構體 實現了context介面,emptyCtx沒有超時時間,不能取消,也不能儲存額外資訊,所以emptyCtx用來做根節點,一般用Background和TODO來初始化emptyCtx ### Backgroud 通常用於主函式,初始化以及測試,作為頂層的context ### TODO 不確定使用什麼用context的時候才會使用 ## valueCtx結構體 ``` type valueCtx struct{ Context key, val interface{} } ``` valueCtx利用Context的變數來表示父節點context,所以當前context繼承了父context的所有資訊 valueCtx還可以儲存鍵值。 ### Value ``` func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } ``` 可以用來獲取當前context和所有的父節點儲存的key >
如果當前的context不存在需要的key,會沿著context鏈向上尋找key對應的值,直到根節點 ### WithValue 可以向context新增鍵值 ``` func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} } ``` 新增鍵值會返回建立一個新的valueCtx子節點 示例程式碼 ``` package main import ( "context" "fmt" "time" ) func main() { ctx := context.WithValue(context.Background(), "top", "root") //第一層 go func(parent context.Context) { ctx = context.WithValue(parent, "cqh", "chenqionghe") //第二層 go func(parent context.Context) { ctx = context.WithValue(parent, "xsfz", "雪山飛豬") //第三層 go func(parent context.Context) { //可以獲取所有的父類的值 fmt.Println(ctx.Value("top")) fmt.Println(ctx.Value("cqh")) fmt.Println(ctx.Value("xsfz")) //不存在 fmt.Println(ctx.Value("xxxx")) }(ctx) }(ctx) }(ctx) time.Sleep(1 * time.Second) fmt.Println("end") } ``` 執行結果 ![](https://img2020.cnblogs.com/blog/662544/202012/662544-20201209164357962-1351246205.png) 可以看到,子context是可以獲取所有父級設定過的key ## cancelCtx結構體 ``` type cancelCtx struct { Context mu sync.Mutex done chan struct{} children map[canceler]struct{} err error } type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } ``` 和valueCtx類似,有一個context做為父節點, 變數done表示一個channel,用來表示傳遞關閉; children表示一個map,儲存了當前context節點為下的子節點 err用來儲存錯誤資訊表示任務結束的原因 ### WithCancel 用來建立一個可取消的context,返回一個context和一個CancelFunc,呼叫CancelFunc可以觸發cancel操作。 ## timerCtx結構體 timerCtx是基於cancelCtx的context精英,是一種可以定時取消的context,過期時間的deadline不晚於所設定的時間d ### WithDeadline 返回一個基於parent的可取消的context,並且過期時間deadline不晚於所設定時間d ### WithTimeout 建立一個定時取消context,和WithDeadline差不多,WithTimeout是相對時間 # 四、總結核心原理 1. Done方法返回一個channel 2. 外部通過呼叫<-channel監聽cancel方法 3. cancel方法會呼叫close(channel) 當呼叫close方法的時間,所有的channel再次從通道獲取內容,會返回零值和false ``` res,ok := <-done: ``` 4. 過期自動取消,使用了time.AfterFunc方法,到時呼叫cancel方法 ``` c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) ``` 授人以漁不如授人以漁,知其然也知其所以然,讓我們共同構建美麗新世界,讓人與自然更加和諧,就是這樣