context背景
因為goroutine,go的併發非常方便,但是這也帶來了另外一個問題,當我們進行一個耗時的非同步操作時,如何在約定的時間內終止該操作並返回一個自定義的結果?這也是大家常說的我們如何去終止一個goroutine(因為goroutine不同於os執行緒,沒有主動interrupt機制),這裡就輪到今天的主角context登場了。
context源於google,於1.7版本加入標準庫,按照官方文件的說法,它是一個請求的全域性上下文,攜帶了截止時間、手動取消等訊號,幷包含一個併發安全的map用於攜帶資料。context的API比較簡單,接下來我會在具體的使用場景中進行介紹。
使用場景一: 請求鏈路傳值
一般來說,我們的根context會在請求的入口處構造如下
ctx := context.Background()
複製程式碼
如果拿捏不準是否需要一個全域性的context,可以使用下面這個函式構造
ctx := context.TODO()
複製程式碼
但是不可以為nil。
傳值使用方式如下
package main
import (
"context"
"fmt"
)
func func1(ctx context.Context) {
ctx = context.WithValue(ctx, "k1", "v1")
func2(ctx)
}
func func2(ctx context.Context) {
fmt.Println(ctx.Value("k1").(string))
}
func main() {
ctx := context.Background()
func1(ctx)
}
複製程式碼
我們在func1通過WithValue(parent Context, key, val interface{}) Context,賦值k1為v1,在其下層函式func2通過ctx.Value(key interface{}) interface{}獲取k1的值,比較簡單。這裡有個疑問,如果我是在func2裡賦值,在func1裡面能夠拿到這個值嗎?答案是不能,context只能自上而下攜帶值,這個是要注意的一點。
使用場景二: 取消耗時操作,及時釋放資源
可以考慮這樣一個問題,如果沒有context包,我們如何取消一個耗時操作呢?我這裡模擬了兩種寫法
- 網路互動場景,經常通過SetReadDeadline、SetWriteDeadline、SetDeadline進行超時取消
timeout := 10 * time.Second
t = time.Now().Add(timeout)
conn.SetDeadline(t)
複製程式碼
- 耗時操作場景,通過select模擬
package main
import (
"errors"
"fmt"
"time"
)
func func1() error {
respC := make(chan int)
// 處理邏輯
go func() {
time.Sleep(time.Second * 3)
respC <- 10
}()
// 超時邏輯
select {
case r := <-respC:
fmt.Printf("Resp: %d\n", r)
return nil
case <-time.After(time.Second * 2):
fmt.Println("catch timeout")
return errors.New("timeout")
}
}
func main() {
err := func1()
fmt.Printf("func1 error: %v\n", err)
}
複製程式碼
以上兩種方式在工程實踐中也會經常用到,下面我們來看下如何使用context進行主動取消、超時取消以及存在多個timeout時如何處理
- 主動取消
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
func func1(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
respC := make(chan int)
// 處理邏輯
go func() {
time.Sleep(time.Second * 5)
respC <- 10
}()
// 取消機制
select {
case <-ctx.Done():
fmt.Println("cancel")
return errors.New("cancel")
case r := <-respC:
fmt.Println(r)
return nil
}
}
func main() {
wg := new(sync.WaitGroup)
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func1(ctx, wg)
time.Sleep(time.Second * 2)
// 觸發取消
cancel()
// 等待goroutine退出
wg.Wait()
}
複製程式碼
- 超時取消
package main
import (
"context"
"fmt"
"time"
)
func func1(ctx context.Context) {
hctx, hcancel := context.WithTimeout(ctx, time.Second*4)
defer hcancel()
resp := make(chan struct{}, 1)
// 處理邏輯
go func() {
// 處理耗時
time.Sleep(time.Second * 10)
resp <- struct{}{}
}()
// 超時機制
select {
// case <-ctx.Done():
// fmt.Println("ctx timeout")
// fmt.Println(ctx.Err())
case <-hctx.Done():
fmt.Println("hctx timeout")
fmt.Println(hctx.Err())
case v := <-resp:
fmt.Println("test2 function handle done")
fmt.Printf("result: %v\n", v)
}
fmt.Println("test2 finish")
return
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
func1(ctx)
}
複製程式碼
對於多個超時時間的處理,可以把上述超時取消例子中的註釋開啟,會觀察到,當處理兩個ctx時,時間短的會優先觸發,這種情況下,如果只判定一個context的Done()也是可以的,但是一定要保證呼叫到兩個cancel函式
注意事項
- context只能自頂向下傳值,反之則不可以。
- 如果有cancel,一定要保證呼叫,否則會造成資源洩露,比如timer洩露。
- context一定不能為nil,如果不確定,可以使用context.TODO()生成一個empty的context。
以上是context剖析的上篇,主要從使用層面,讓大家有一個直觀的認識,這樣在工程中可以進行靈活的使用,接下來會從原始碼層面進行剖析。