go context剖析之使用技巧

context背景

由於goroutine,go的併發很是方便,可是這也帶來了另一個問題,當咱們進行一個耗時的異步操做時,如何在約定的時間內終止該操做並返回一個自定義的結果?這也是你們常說的咱們如何去終止一個goroutine(由於goroutine不一樣於os線程,沒有主動interrupt機制),這裏就輪到今天的主角context登場了。git

context源於google,於1.7版本加入標準庫,按照官方文檔的說法,它是一個請求的全局上下文,攜帶了截止時間、手動取消等信號,幷包含一個併發安全的map用於攜帶數據。context的API比較簡單,接下來我會在具體的使用場景中進行介紹。github

使用場景一: 請求鏈路傳值

通常來講,咱們的根context會在請求的入口處構造以下golang

ctx := context.Background()
複製代碼

若是拿捏不許是否須要一個全局的context,可使用下面這個函數構造安全

ctx := context.TODO()
複製代碼

可是不能夠爲nilbash

傳值使用方式以下網絡

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函數ui

注意事項

  • context只能自頂向下傳值,反之則不能夠。
  • 若是有cancel,必定要保證調用,不然會形成資源泄露,好比timer泄露。
  • context必定不能爲nil,若是不肯定,可使用context.TODO()生成一個empty的context。

以上是context剖析的上篇,主要從使用層面,讓你們有一個直觀的認識,這樣在工程中能夠進行靈活的使用,接下來會從源碼層面進行剖析。

參考資料

golang官方包

Go Concurrency Patterns: Context

etcd客戶端超時處理示例代碼

相關文章
相關標籤/搜索