這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前瞻
context 包困擾我好久,之前在 watch etcd 的時候首次上手使用這個包,當時並不理解這個包的作用,只知道可以用來關閉 watch , 後來被大牛吐槽了,決定深入探究一番。
簡介
golang 中的建立一個新的 goroutine , 並不會返回像c語言類似的pid,所有我們不能從外部殺死某個goroutine,所有我就得讓它自己結束,之前我們用 channel + select 的方式,來解決這個問題,但是有些情境實現起來比較麻煩,例如由一個請求衍生出的各個 goroutine 之間需要滿足一定的約束關係,以實現一些諸如有效期間,中止routine樹,傳遞請求全域變數之類的功能。於是google 就為我們提供一個解決方案,開源了 context 包。使用 context 實現上下文功能約定需要在你的方法的傳入參數的第一個傳入一個 context.Context 類型的變數。
源碼剖析
context.Context 介面
context 包的核心
// context 包裡的方法是安全執行緒的,可以被多個 goroutine 使用 type Context interface { // 當Context 被 canceled 或是 times out 的時候,Done 返回一個被 closed 的channel Done() <-chan struct{} // 在 Done 的 channel被closed 後, Err 代表被關閉的原因 Err() error // 如果存在,Deadline 返回Context將要關閉的時間 Deadline() (deadline time.Time, ok bool) // 如果存在,Value 返回與 key 相關了的值,不存在返回 nil Value(key interface{}) interface{}}
我們不需要手動實現這個介面,context 包已經給我們提供了兩個,一個是 Background(),一個是 TODO(),這兩個函數都會返回一個 Context 的執行個體。只是返回的這兩個執行個體都是空 Context。
主要結構
cancelCtx 結構體繼承了 Context ,實現了 canceler 方法:
//*cancelCtx 和 *timerCtx 都實現了canceler介面,實現該介面的類型都可以被直接canceledtype 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]bool // 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的概念 }}
timerCtx 結構繼承 cancelCtx
type timerCtx struct { cancelCtx //此處的封裝為了繼承來自於cancelCtx的方法,cancelCtx.Context才是父親節點的指標 timer *time.Timer // Under cancelCtx.mu. 是一個計時器 deadline time.Time}
valueCtx 結構繼承 cancelCtx
type valueCtx struct { Context key, val interface{}}
主要方法
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 interface{}, val interface{}) Context
WithCancel 對應的是 cancelCtx ,其中,返回一個 cancelCtx ,同時返回一個 CancelFunc,CancelFunc 是 context 包中定義的一個函數類型:type CancelFunc func()。調用這個 CancelFunc 時,關閉對應的c.done,也就是讓他的後代goroutine退出。
WithDeadline 和 WithTimeout 對應的是 timerCtx ,WithDeadline 和 WithTimeout 是相似的,WithDeadline 是設定具體的 deadline 時間,到達 deadline 的時候,後代 goroutine 退出,而 WithTimeout 簡單粗暴,直接 return WithDeadline(parent, time.Now().Add(timeout))。
WithValue 對應 valueCtx ,WithValue 是在 Context 中設定一個 map,拿到這個 Context 以及它的後代的 goroutine 都可以拿到 map 裡的值。
詳細 context 包源碼解讀:go源碼解讀
使用原則
使用 Context 的程式包需要遵循如下的原則來滿足介面的一致性以及便於靜態分析
不要把 Context 存在一個結構體當中,顯式地傳入函數。Context 變數需要作為第一個參數使用,一般命名為ctx
即使方法允許,也不要傳入一個 nil 的 Context ,如果你不確定你要用什麼 Context 的時候傳一個 context.TODO
使用 context 的 Value 相關方法只應該用於在程式和介面中傳遞的和請求相關的中繼資料,不要用它來傳遞一些可選的參數
同樣的 Context 可以用來傳遞到不同的 goroutine 中,Context 在多個goroutine 中是安全的
使用樣本
例子copy自: 關於 Golang 中的 context 包的介紹
package mainimport ( "fmt" "time" "golang.org/x/net/context")// 類比一個最小執行時間的阻塞函數func inc(a int) int { res := a + 1 // 雖然我只做了一次簡單的 +1 的運算, time.Sleep(1 * time.Second) // 但是由於我的機器指令集中沒有這條指令, // 所以在我執行了 1000000000 條機器指令, 續了 1s 之後, 我才終於得到結果。B) return res}// 向外部提供的阻塞介面// 計算 a + b, 注意 a, b 均不能為負// 如果計算被中斷, 則返回 -1func Add(ctx context.Context, a, b int) int { res := 0 for i := 0; i < a; i++ { res = inc(res) select { case <-ctx.Done(): return -1 default: } } for i := 0; i < b; i++ { res = inc(res) select { case <-ctx.Done(): return -1 default: } } return res}func main() { { // 使用開放的 API 計算 a+b a := 1 b := 2 timeout := 2 * time.Second ctx, _ := context.WithTimeout(context.Background(), timeout) res := Add(ctx, 1, 2) fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res) } { // 手動取消 a := 1 b := 2 ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(2 * time.Second) cancel() // 在調用處主動取消 }() res := Add(ctx, 1, 2) fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res) }}