Context
在 Go 服務中,往往由一個獨立的 goroutine 去處理一次請求,但在這個 goroutine 中,可能會開啟別的 goroutine 去執行一些具體的事務,如數據庫,RPC 等,同時,這一組 goroutine 可能還需要共同訪問一些特殊的值,如用戶 token, 請求過期時間等,當一個請求超時后,我們希望與此請求有關的所有 goroutine 都能快速退出,以回收系統資源。
context 包由谷歌開源,在 Go 1.7 時加入標準庫,使用它可以很容易的把特定的值,取消信號, 截止日期傳遞給請求所涉及的所有 goroutine。
context 包的核心是 Context
接口,其結構如下:
type Context interface {Done() <-chan struct{}Err() errorDeadline() (deadline time.Time, ok bool)Value(key interface{}) interface{}
}
Done
返回一個chan
, 表示一個取消信號,當這個通道被關閉時,函數應該立刻結束工作并返回。Err()
返回一個error
, 表示取消上下文的原因Deadline
會返回上下文取消的時間Value
用于從上下文中獲取key
對應的值
使用
傳遞取消信號(cancelation signals)
正如使用 chan
控制并發一樣,我們希望傳遞給 goroutine 一個信號,一旦接收到這個信號,就立刻停止工作并返回,context 包提供了一個 WithCancel()
, 使用它可以很方便的傳遞取消信號。
func useContext(ctx context.Context, id int) {for {select {case <- ctx.Done():fmt.Println("stop", id)returndefault:run(id)}}
}func G2(ctx context.Context) {nCtx, nStop := context.WithCancel(ctx)go G4(nCtx)for {select {case <- ctx.Done():fmt.Println("stop 2")nStop()returndefault:run(2)}}
}func G3(ctx context.Context) {useContext(ctx, 3)
}func G4(ctx context.Context) {useContext(ctx, 4)
}func main() {ctx, done := context.WithCancel(context.Background())go G2(ctx)go G3(ctx)time.Sleep(5*time.Second)done()time.Sleep(5*time.Second)
}
設置截止時間
func G6(ctx context.Context) {for {select {case <- ctx.Done():t, _ := ctx.Deadline()fmt.Printf("[*] %v done: %v\n", t, ctx.Err())returndefault:fmt.Println("[#] run ...")}}
}func main() {// ctx, done := context.WithTimeout(context.Background(), time.Second * 2)ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)go G6(ctx)//done()time.Sleep(10*time.Second)
}[#] run ...
...
[*] 2020-10-31 20:24:42.0581352 +0800 CST m=+2.008975001 done: context deadline exceeded
傳值
func G7(ctx context.Context) {for {select {case <- ctx.Done():fmt.Println("cancel", ctx.Value("key"))returndefault:fmt.Println("running ", ctx.Value("key"))time.Sleep(time.Second)}}
}func main() {ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)ctx = context2.WithValue(ctx, "key", "value")go G7(ctx)time.Sleep(10*time.Second)
}
context 包概覽
context 包的核心是 context.Context
接口,另外有四個 struct
實現了 Context
接口,分別是 emptyCtx
, cancelCtx
, timerCtx
, valueCtx
, 其中 emptyCtx
是一個默認的空結構體,其余三個都是在其基礎上添加了各自功能的實現,針對 emptyCtx
,context 包中暴露了兩個方法 Background()
和 TODO()
去創建一個空的 emptyCtx
, 而針對后面三種具體的 struct
,context 包總共暴露了四個方法去產生對應的 struct
, 他們分別是: WithCancel()
, WithDeadLine()
, WithTimeout()
, WithValue()
,對應關系如下:

TODO 和 Background
TODO 和 Background 方法用來返回一個 emptyCtx
類型,他們在實現上都一樣:
var (background = new(emptyCtx)todo = new(emptyCtx)
)func Background() Context {return background
}func TODO() Context {return todo
}
這兩個方法都會返回一個非空的上下文 emptyCtx
,他永遠不會被取消,用于傳遞給其他方法去構建更加復雜的上下文對象,一般默認使用 Background()
, 只有在不確定時使用TODO()
, 但實際上他們只是名字不同而已。
下面是 emptyCtx
的實現,他確實沒做任何事。
type emptyCtx intfunc (*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
}
WithCancel
type cancelCtx struct {Contextmu sync.Mutex // 用于同步done chan struct{} // 會在 Done 中返回children map[canceler]struct{} // 子上下文列表,done 被關閉后,會遍歷這個 map,關閉所有的子上下文err error // 關閉 chan 產生的異常,在初始化時會被賦值使不為空
}func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}
當調用 WithCancel
時, 首先會根據 parent
拷貝一個新的 cancelCtx
:
func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}
然后會調用 propagateCancel
安排子上下文在父上下文結束時結束,最后除了 cancelCtx
的引用外還會返回一個 func
, 該方法里調用了 c.cancel()
, 也就是當我們調用 done()
時,調用的其實是 c.cancel()
cancel
cancel
的作用是關閉 當前上下文以及子上下文的cancelCtx.done
管道。
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 // 已經關閉,返回}c.err = err // 通過 err 標識已經關閉if c.done == nil {c.done = closedchan} else {close(c.done) // 關閉當前 done}// 由于是 map, 所以關閉順序是隨機的for child := range c.children {child.cancel(false, err) // 遍歷取消所有子上下文}c.children = nil // 刪除子上下文c.mu.Unlock()if removeFromParent {removeChild(c.Context, c) // 從父上下文刪除自己}
}
propagateCancel
該函數的作用是保證父上下文結束時子上下文也結束,一方面,在生成子上下文的過程中,如果父親已經被取消,那 child
也會被關閉,另一方面,如果在執行過程中父上下文一直開啟,那就正常把子上下文加入到父上下文的 children
列表中等執行 cancel
再關閉。
func propagateCancel(parent Context, child canceler) {done := parent.Done()// 如果父親的 Done 方法返回空,說明父上下文永遠不會被取消// 這種情況對應 ctx, done := context.WithCancel(context.Background())if done == nil {return }// 如果到這父上下文已經被取消了,就關閉當前上下文select {case <-done:child.cancel(false, parent.Err())returndefault:}// 父親沒有被取消if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()// 父親已經取消,關閉自己if p.err != nil {child.cancel(false, p.err)} else {// 把 child 加到 parent 的 children 中if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {// 父上下文是開發者自定義的類型, 開啟一個 goroutine 監聽父子上下文直到其中一個關閉atomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}
WithTimeout 和 WithDeadline
type timerCtx struct {cancelCtxtimer *time.Timerdeadline time.Time
}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}
timerCtx
是在 cancelCtx
的基礎上添加了一個定時器和截止時間實現的。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {// 如果傳入的截止時間比父上下文的截止時間晚,也就是說父上下文一定會比子上下文先結束// 這種情況下給子上下文設置截止時間是沒有任何意義的,所以會直接創建一個 cancelCtxif cur, ok := parent.Deadline(); ok && cur.Before(d) {return WithCancel(parent)}// 構建新的 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline: d,}// 保證子上下文在父上下文關閉時關閉propagateCancel(parent, c)// 計算當前距離截止時間 d 還有多長時間dur := time.Until(d)// 如果已經過了截止時間,關閉子上下文if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()// c.err == nil 說明當前上下文還沒有被關閉if c.err == nil {// AfterFunc 等待 dur 后會開啟一個 goroutine 執行 傳入的方法,即 c.cancel// 并會返回一個計時器 timer,通過調用 timer 的 Stop 方法可以停止計時取消調用。c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
timerCtx
的 cancel
方法主要還是調用了 cancelCtx.cancel
func (c *timerCtx) cancel(removeFromParent bool, err error) {// 調用 cancelCtx.cancel,關閉子上下文c.cancelCtx.cancel(false, err)// 從父上下文中刪除當前上下文if removeFromParent {removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {// 停止計時,取消調用c.timer.Stop()c.timer = nil}c.mu.Unlock()
}
WithTimeout
直接調用了 WithDeadline
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
WithValue
func WithValue(parent Context, key, val interface{}) Context {// key 不能為 nilif key == nil {panic("nil key")}// key 必須是可比較的if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}type valueCtx struct {Contextkey, val interface{}
}
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys.
key 請盡量使用自定義的 struct{}, 避免使用內置數據類型以避免使用 context 包時的沖突
總結
context 包是 Go 1.7 后加入的一種用于復雜場景下并發控制的模型,最核心的接口是 context.Context
, 這個結構體中定義了五個待實現的方法,用來實現發送關閉信號,設置 dateline,傳遞值等功能。
context 包的核心思想是以 樹形 組織 goroutine, 創建新上下文時需要給他指定一個父上下文,由此,根上下文對應根 goroutine, 子上下文對應子 Goroutine, 實現靈活的并發控制。
rootContext 一般通過 Background()
或 TODO()
創建,他們會創建一個空的 emptyCtx
, 然后如果想要使用 context 包的具體功能,可以使用 WithCancel()
, WithDateline()
或 WithValue()
將父上下文包裝成具體的上下文對象(cancelCtx, timerCtx, valueCtx
),前兩個方法會返回兩個值 (ctx Context, done func())
調用 done
可以向 goroutine 發送一個關閉信號, goroutine 中監控 ctx.Done()
便可得到這個信號。
cancelCtx
和 timerCtx
會保持一個 children
(timerCtx
實際上是繼承了 cancelCtx
),這是一個 map
key 是 canceler
, Value 是 struct{}
類型,值并沒什么用,在創建 cancelCtx
或 timerCtx
時,會把當前上下文加入到其父親的 children
中,在父上下文關閉時會遍歷 children
關閉所有的子上下文,并將本上下文從其父上下文的 children
中刪除,由于 map
遍歷的無序性,子上下文關閉的順序也是隨機的。
WithValue()
以及 valueCtx
的實現稍微與前兩個有所不同,一方面 valueCtx
沒有自己實現 Done(), Deadline()
等方法,所以其功能僅限于傳值,另外,在 WithValue()
中并沒有調用 propagateCancel()
, 所以 valueCtx
并不會被放在父上下文的 children
中,他自己也沒有 children
, 所以使用 valueCtx
作為父上下文是沒有意義的。
如非必要,一般無需使用 WithValue()
的功能傳值,他一般用在傳遞請求對應用戶的認證令牌或用于進行分布式追蹤的請求 ID中。