Golang Context深刻理解

[TOC]html

Golang Context深刻理解

Context背景 和 適用場景

golang在1.6.2的時候尚未本身的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來簡化對於處理單個請求的多個goroutine之間與請求域的數據、取消信號、截止時間等相關操做,這些操做可能涉及多個 API 調用。golang

好比有一個網絡請求Request,每一個Request都須要開啓一個goroutine作一些事情,這些goroutine又可能會開啓其餘的goroutine。這樣的話, 咱們就能夠經過Context,來跟蹤這些goroutine,而且經過Context來控制他們的目的,這就是Go語言爲咱們提供的Context,中文能夠稱之爲「上下文」。數據庫

另一個實際例子是,在Go服務器程序中,每一個請求都會有一個goroutine去處理。然而,處理程序每每還須要建立額外的goroutine去訪問後端資源,好比數據庫、RPC服務等。因爲這些goroutine都是在處理同一個請求,因此它們每每須要訪問一些共享的資源,好比用戶身份信息、認證token、請求截止時間等。並且若是請求超時或者被取消後,全部的goroutine都應該立刻退出而且釋放相關的資源。這種狀況也須要用Context來爲咱們取消掉全部goroutine後端

若是要使用能夠經過 go get golang.org/x/net/context 命令獲取這個包。安全

Context 定義

ontext的主要數據結構是一種嵌套的結構或者說是單向的繼承關係的結構,好比最初的context是一個小盒子,裏面裝了一些數據,以後從這個context繼承下來的children就像在本來的context中又套上了一個盒子,而後裏面裝着一些本身的數據。或者說context是一種分層的結構,根據使用場景的不一樣,每一層context都具有有一些不一樣的特性,這種層級式的組織也使得context易於擴展,職責清晰。bash

context 包的核心是 struct Context,聲明以下:服務器

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}
複製代碼

能夠看到Context是一個interface,在golang裏面,interface是一個使用很是普遍的結構,它能夠接納任何類型。Context定義很簡單,一共4個方法,咱們須要可以很好的理解這幾個方法微信

  1. Deadline方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,若是須要取消的話,須要調用取消函數進行取消。網絡

  2. Done方法返回一個只讀的chan,類型爲struct{},咱們在goroutine中,若是該方法返回的chan能夠讀取,則意味着parent context已經發起了取消請求,咱們經過Done方法收到這個信號後,就應該作清理操做,而後退出goroutine,釋放資源。以後,Err 方法會返回一個錯誤,告知爲何 Context 被取消。數據結構

  3. Err方法返回取消的錯誤緣由,由於什麼Context被取消。

  4. Value方法獲取該Context上綁定的值,是一個鍵值對,因此要經過一個Key才能夠獲取對應的值,這個值通常是線程安全的。

Context 的實現方法

Context 雖然是個接口,可是並不須要使用方實現,golang內置的context 包,已經幫咱們實現了2個方法,通常在代碼中,開始上下文的時候都是以這兩個做爲最頂層的parent context,而後再衍生出子context。這些 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的時候,可使用這個,可是實際應用中,暫時尚未使用過這個TODO。

他們兩個本質上都是emptyCtx結構體類型,是一個不可取消,沒有設置截止時間,沒有攜帶任何值的Context。

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
}
複製代碼

Context 的 繼承

有了如上的根Context,那麼是如何衍生更多的子Context的呢?這就要靠context包爲咱們提供的With系列的函數了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithValue(parent Context, key, val interface{}) Context

複製代碼

經過這些函數,就建立了一顆Context樹,樹的每一個節點均可以有任意多個子節點,節點層級能夠有任意多個。

WithCancel函數,傳遞一個父Context做爲參數,返回子Context,以及一個取消函數用來取消Context。

WithDeadline函數,和WithCancel差很少,它會多傳遞一個截止時間參數,意味着到了這個時間點,會自動取消Context,固然咱們也能夠不等到這個時候,能夠提早經過取消函數進行取消。

WithTimeout和WithDeadline基本上同樣,這個表示是超時自動取消,是多少時間後自動取消Context的意思。

WithValue函數和取消Context無關,它是爲了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據能夠經過Context.Value方法訪問到,這是咱們實際用常常要用到的技巧,通常咱們想要經過上下文來傳遞數據時,能夠經過這個方法,如咱們須要tarce追蹤系統調用棧的時候。

With 系列函數詳解

WithCancel

context.WithCancel生成了一個withCancel的實例以及一個cancelFuc,這個函數就是用來關閉ctxWithCancel中的 Done channel 函數。

下面來分析下源碼實現,首先看看初始化,以下:

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{
		Context: parent,
		done:    make(chan struct{}),
	}
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
複製代碼

newCancelCtx返回一個初始化的cancelCtx,cancelCtx結構體繼承了Context,實現了canceler方法:

//*cancelCtx 和 *timerCtx 都實現了canceler接口,實現該接口的類型均可以被直接canceled
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}


type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error             // 當其被cancel時將會把err設置爲非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

//核心是關閉c.done
//同時會設置c.err = err, c.children = nil
//依次遍歷c.children,每一個child分別cancel
//若是設置了removeFromParent,則將c從其parent的children中刪除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    close(c.done)
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c) // 今後處能夠看到 cancelCtx的Context項是一個相似於parent的概念
    }
}
複製代碼

能夠看到,全部的children都存在一個map中;Done方法會返回其中的done channel, 而另外的cancel方法會關閉Done channel而且逐層向下遍歷,關閉children的channel,而且將當前canceler從parent中移除。

WithCancel初始化一個cancelCtx的同時,還執行了propagateCancel方法,最後返回一個cancel function。

propagateCancel 方法定義以下:

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
複製代碼

propagateCancel 的含義就是傳遞cancel,從當前傳入的parent開始(包括該parent),向上查找最近的一個能夠被cancel的parent, 若是找到的parent已經被cancel,則將方纔傳入的child樹給cancel掉,不然,將child節點直接鏈接爲找到的parent的children中(Context字段不變,即向上的父親指針不變,可是向下的孩子指針變直接了); 若是沒有找到最近的能夠被cancel的parent,即其上都不可被cancel,則啓動一個goroutine等待傳入的parent終止,則cancel傳入的child樹,或者等待傳入的child終結。

WithDeadLine

在withCancel的基礎上進行的擴展,若是時間到了以後就進行cancel的操做,具體的操做流程基本上與withCancel一致,只不過控制cancel函數調用的時機是有一個timeout的channel所控制的。

Context 使用原則 和 技巧

  • 不要把Context放在結構體中,要以參數的方式傳遞,parent Context通常爲Background
  • 應該要把Context做爲第一個參數傳遞給入口請求和出口請求鏈路上的每個函數,放在第一位,變量名建議都統一,如ctx。
  • 給一個函數方法傳遞Context的時候,不要傳遞nil,不然在tarce追蹤的時候,就會斷了鏈接
  • Context的Value相關方法應該傳遞必須的數據,不要什麼數據都使用這個傳遞
  • Context是線程安全的,能夠放心的在多個goroutine中傳遞
  • 能夠把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操做時,全部 goroutine 都會接收到取消信號。

Context的經常使用方法實例

  1. 調用Context Done方法取消

    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:
    		}
    	}
    }
    
    複製代碼
  2. 經過 context.WithValue 來傳值

    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    
    	valueCtx := context.WithValue(ctx, key, "add value")
    
    	go watch(valueCtx)
    	time.Sleep(10 * time.Second)
    	cancel()
    
    	time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context) {
    	for {
    		select {
    		case <-ctx.Done():
    			//get value
    			fmt.Println(ctx.Value(key), "is cancel")
    
    			return
    		default:
    			//get value
    			fmt.Println(ctx.Value(key), "int goroutine")
    
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    
    複製代碼
  3. 超時取消 context.WithTimeout

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    
    	"golang.org/x/net/context"
    )
    
    var (
    	wg sync.WaitGroup
    )
    
    func work(ctx context.Context) error {
    	defer wg.Done()
    
    	for i := 0; i < 1000; i++ {
    		select {
    		case <-time.After(2 * time.Second):
    			fmt.Println("Doing some work ", i)
    
    		// we received the signal of cancelation in this channel
    		case <-ctx.Done():
    			fmt.Println("Cancel the context ", i)
    			return ctx.Err()
    		}
    	}
    	return nil
    }
    
    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
    	defer cancel()
    
    	fmt.Println("Hey, I'm going to do some work")
    
    	wg.Add(1)
    	go work(ctx)
    	wg.Wait()
    
    	fmt.Println("Finished. I'm going home")
    }
    
    複製代碼
  4. 截止時間 取消 context.WithDeadline

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	d := time.Now().Add(1 * time.Second)
    	ctx, cancel := context.WithDeadline(context.Background(), d)
    
    	// Even though ctx will be expired, it is good practice to call its
    	// cancelation function in any case. Failure to do so may keep the
    	// context and its parent alive longer than necessary.
    	defer cancel()
    
    	select {
    	case <-time.After(2 * time.Second):
    		fmt.Println("oversleep")
    	case <-ctx.Done():
    		fmt.Println(ctx.Err())
    	}
    }
    
    複製代碼

參考

飛雪無情的博客

【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】

個人微信公衆號
相關文章
相關標籤/搜索