Golang Context 詳細介紹

Golang context

  本文包含對context實現上的分析和使用方式,分析部分源碼講解比價多,可能會比較枯燥,讀者能夠直接跳過去閱讀使用部分。golang

  ps: 做者本着開源分享的精神撰寫本篇文章,若是出現任何偏差務必留言指正,做者會在第一時間內修正,共同維護一個好的開源生態,謝謝!!!ide

1、簡介

  做者所講的context的包名稱是: "golang.org/x/net/context" ,但願讀者不要引用錯誤了。函數

  在godoc中對context的介紹以下:ui

 Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

 As of Go 1.7 this package is available in the standard library under the name context. https://golang.org/pkg/context. 

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between must propagate the Context, optionally replacing it with a modified copy created using WithDeadline, WithTimeout, WithCancel, or WithValue.

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

See http://blog.golang.org/context for example code for a server that uses Contexts.
View Code

  鑑於做者英文水平有限,在這裏不進行對照翻譯,以避免誤導讀者。它的第一句已經介紹了它的做用了:一個貫穿API的邊界和進程之間的context 類型,能夠攜帶deadlines、cancel signals和其餘信息。就如同它的中文翻譯同樣:上下文。在一個應用服務中會並行運行不少的goroutines或進程, 它們彼此之間或者是從屬關係、競爭關係、互斥關係,不一樣的goroutines和進程進行交互的時候須要進行狀態的切換和數據的同步,而這就是context包要支持的功能。
this

2、解析

  context的接口定義以下: spa

   每個接口都有詳細的註釋,這裏就不重複了。 在context的源碼中有如下幾個結構體實現了Context Interface:翻譯

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   // Deadline returns the time when work done on behalf of this context
   // should be canceled.  Deadline returns ok==false when no deadline is
   // set.  Successive calls to Deadline return the same results.
   Deadline() (deadline time.Time, ok bool)
   // Done returns a channel that's closed when work done on behalf of this
   // context should be canceled.  Done may return nil if this context can
   // never be canceled.  Successive calls to Done return the same value.
   Done() <-chan struct{}
   // Err returns a non-nil error value after Done is closed.  Err returns
   // Canceled if the context was canceled or DeadlineExceeded if the
   // context's deadline passed.  No other values for Err are defined.
   // After Done is closed, successive calls to Err return the same value.
   Err() error
   // Value returns the value associated with this context for key, or nil
   // if no value is associated with key.  Successive calls to Value with
   // the same key returns the same result.
   Value(key interface{}) interface{}
}

2.1 empty context

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

  這是一個空的ctx類型,每個返回值都爲空,它什麼都功能都不具有,主要的做用是做爲全部的context類型的起始點,context.Background()函數返回的就是這中類型的Context:指針

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
    return background
}

   empty context的做用做者下面會闡述。 code

2.2 cancle context

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

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)
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's 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
    if c.done == nil {
        c.done = closedchan
    } else {
        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中的成員變量:server

     `done chan struct{}`: 在調用Done()函數時會將該變量返回,這能夠用在多goroutines之間進行狀態同步; 

    `children map[canceler]struct{}`: 一個context能夠被其餘context 引用,被引用的context爲parant,引用context爲children,該變量包含全部的children context;

    `Context`:  關聯了Context接口,實現了Done()和Err(),可是沒有實現Value()和Deadline();

      cancel函數是一個受保護的函數,不能在外部進行調用。能夠看到在執行這個函數的時候 done channel會被關閉掉,同時它會調用全部的children context的cancel函數,可是沒有解除彼此的依賴關係。這實際上比較好理解,由於children context的生命週期是依賴與parant context的。同時它還要判斷是否調用 removeChild(c.Context, c)函數將解除對parant context的引用關係。

   在context.WithCancel(parent Context) 函數中返回的就是cancelCtx;

   

2.3 timer context

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.
	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

   timer context 中的成員變量:

    `cancelCtx`: timerCtx 關聯了 canelCtx類型;

    `timer`:  一個定時器,用來設置超時的時間;

    `dealine`: 一個時間類型,用來記錄死亡時間;

  cancel函數 :1)它會先觸發c.cancelCtx的cancel操做,但沒有解除c.cancelCtx與parentCtx的依賴關係。2)判斷是否去解除自身對於parentCtx的依賴, 3)中止它的timer,這個時候計時就結束了;

  它只實現了Deadline()函數,值得注意的是timeCtx和其包含的cancleCtx 是依賴於同一個parentCtx的。

  在WithDeadline(parent Context, deadline time.Time)和WithTimeout(parent Context, timeout time.Duration)函數中返回的就是 timerCtx;

2.4 value context  

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) String() string {
	return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

   value context成員變量:

    Context:它關聯了Context接口

    key,val interface{} :兩個變量造成一個key-value結構;

   它只實現了Value()函數,能夠返回key對應的value,這裏須要注意的是,在查詢value的時候,若是當前context中沒有,會向上級的context進行搜索,遞歸查詢

  WithValue(parent Context, key, val interface{}) 函數返回的即爲value context。

2.5 總結

  綜上四種類型的context 總結以下:

Name Deadline Done Err Value 繼承
empty + + + + nil
cancel - + + - Context
timer + - - - canelCtx
value - - - + Context

  咱們能夠發現除了empty之外其餘三種類型都沒有徹底去實現Context接口定義的全部函數,若是直接實例化一個cancelCtx對象,可是沒有對 Context部分進行賦值,當調用其Value和Deadline函數會崩潰,timerCtx和valueCtx也是一樣的道理。請讀者必定要記住以上四種類型,這樣你會很容易理解下面的內容。

 3、 Context的使用

3.1 Context 經常使用函數

   咱們在上面的介紹過程當中提到了不少函數:

//建立一個Cancel context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
//建立一個帶有 deadline的Timer context func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
//建立一個帶有超時的Timer context func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
//建立一個Value context func WithValue(parent Context, key, val interface{}) Context

   這些函數都是在使用Context中常常用到的,咱們接下來講明它們的功能。

WithCancel

介紹:

  複製parentCtx,同時建立一個新的Done Channel返回Cancel函數,當如下兩種狀況發生時會關閉Done Channel,觸發Done信號:

  1) 返回的Cancel函數被調用;

  2) parentCtx觸發了Done信號;

  一般一個Context生命週期截止了(再也不被須要的時候)就要馬上調用Cancel函數

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

   在newcancel函數中實例化一個cancel context對象:  

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

   propagateCancel如註釋所述:向上找到最近的能夠被依賴的父context,將子context放入 parent的children隊列;若是找不到就開一個goroutines來等待主鏈退出的通知。

// 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():
			}
		}()
	}
}

  parentCancelCtx : 尋找沿着parant引用向上追溯,直到發現一個cancelCtx;

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   for {
      switch c := parent.(type) {
      case *cancelCtx:
         return c, true
      case *timerCtx:
         return &c.cancelCtx, true
      case *valueCtx:
         parent = c.Context
      default:
         return nil, false
      }
   }
}

   它還返回一個函數指針,這個函數指針實際上就是執行cancelCtx中的cancel函數--c.cancel(true, Canceled),該操做會解除和parent ctx的依賴。

   總的來講建立一個新的context就是在parant context中掛載一個 children context,也許傳入的parent與新生成的ctx會掛載到同一個ctx下,也許會加入到parent contxt的children 隊列中。咱們要與上面的四種類型的context比較,empty context和 value context是不具有掛載children的能力的,而cancel context 和timer context 兩種類型具有掛載chidren 的能力(實際上timerCtx的掛載能力是繼承於canelCtx)。

  但問題來了,在建立cancel context時候須要傳入一個parent 參數,那麼這個parent從哪裏來?這時候就須要 func Background() Context 這個函數,它返回一個做爲起始點的context對象,而這個BackgroundCtx是一個empty context,這就是empty context的做用。在回想一下上面的介紹是否是很合理的構造?

WithDeadline

  複製parentCtx,在建立的時候若是parentCtx已經觸發Deadline,則直接返回一個cancelCtx和Cancel函數,不然會將返回一個timeCtx和Cancel函數。在發生如下狀況的時候Done信號會被觸發:

  1)Cancel函數被調用

  2)當前時間超過設置的Deadline

  3)parentCtx觸發了Done信號

  

 

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(true, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

 

WithValue

  複製parentCtx,保存傳入的key-value鍵值對,沒有Cancel函數被返回,但不表明該context沒有done信號,只有在parentCtx的Done信號被觸發的時候,纔會發送。

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}
}

  

 

 

 3.2 、Context的應用示例

 

參考網址   

[1] https://godoc.org/golang.org/x/net/context

相關文章
相關標籤/搜索