context
context譯名為上下文,其設計的初衷是用來跨多個協程尤其是父子協程間傳遞信號和一些簡單的數據。它通常在處理 HTTP 請求、任務調度、數據庫查詢等場景中使用,尤其是在微服務架構中,gRPC 就通過context來跨進程跨網絡來做元數據傳遞,鏈路控制等操作。
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
defer stop()
for {
select {
case <-ctx.Done():
fmt.Println("terminate")
return
default:
}
fmt.Println("running")
time.Sleep(100 * time.Millisecond)
}
}上面這個案例中,通過context來傳遞進程信號,當接收到信號後,程序就會自行退出,這也是context的一個應用場景。
結構
context.context並不是一個具體的實現,而是一個定義了一組方法的接口
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(),返回一個截至日期,和布爾值表示是否已經結束Done(),返回一個管道,用於通知結束消息Err(),返回上下文關閉的原因Value(),根據 key 獲取指定的值
標准庫中總共有下面幾個可用的上下文
Background,空白的上下文,通常用於上下文的根節點WithCancel,WithCancelCause,可取消的上下文WithDeadline,WithDeadlineCause,帶截至時間的上下文WithTimeout,WithTimeoutCause,帶超時時間的上下文WithValue,可以傳值的上下文
具體到實現就主要是
timerCtxcancelCtxemptyCtx
所以它的核心功能就四個點
- 取消
- 截止
- 傳值
- 傳播
弄懂了這幾個,基本上就明白了context的運作原理。
取消
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of 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
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}cancelCtx的核心就在於propagateCancel這個方法,它負責將可取消這一行為傳播到父子上下文中。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}首先它會調用檢查父 context 能否被取消,不能的話就直接返回
godone := parent.Done() if done == nil { return // parent is never canceled }返回檢查父 context 是否已經被取消了,是的話則取消所有的子 context
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }嘗試將父 context 轉換為
cancelCtx類型,如果成功了的話就會將當前 context 加入到父 context 的children中。goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err, p.cause) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return }嘗試將其轉換為
afterFuncer類型,如果成功了的話,就會將取消當前 context 的方法注冊到父 context 的AfterFunc中goif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. c.mu.Lock() stop := a.AfterFunc(func() { child.cancel(false, parent.Err(), Cause(parent)) }) c.Context = stopCtx{ Context: parent, stop: stop, } c.mu.Unlock() return }如果還是不行,那麼就單獨開一個協程來監聽
Done管道,當收到信號時,就會取消子 contextgogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
然後由cancelCtx.cancel方法最終來負責取消子 context
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}它的流程如下
檢查當前 context 是否已經被取消
goif err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled }關閉
done管道,發送關閉通知goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }遍歷通知子 context
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()最根據傳入的參數判斷是否需要從父 context 中刪除
if removeFromParent { removeChild(c.Context, c) }
截止
WithTimeout和WithDeadline都是具有截至時間的上下文,兩者都是一個類型,僅僅只是使用的語義不一樣,且都基於cancelCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause負責創建具有截至時間的上下文
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}它的流程如下
檢查截至日期,如果父截止日期比當前截止日期早,那麼父 context 肯定比當前 context 先取消,則直接創建
cancelCtx類型的上下文goif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }構建
timeCtx,並傳播到子 contextgoc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)計算當前截止時間,如果已經截止了就直接取消
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }如果沒有截止的話,就通過
time.AfterFunc設置在截至時間取消當前 contextgoc.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) }
對於timerCtx而言,它取消上下文方法就只是將timer停止,然後順便停止子 context
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
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()
}傳值
valueCtx可以在上下文中傳值,獲取值
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}其核心就在於value函數,其它的 context 具體實現也會進入到該函數來取值,比如
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}value函數是一個大的 for 循環,通過不斷向上遞歸來尋找指定 key 的值
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}不同 Context 類型的處理:
*valueCtx:如果key匹配當前Context的key,則返回與key對應的val。*cancelCtx:如果key匹配cancelCtxKey,則返回當前的cancelCtx,表示返回其自身。withoutCancelCtx:表示沒有取消功能的Context,如果key匹配cancelCtxKey,則返回nil。*timerCtx:如果key匹配cancelCtxKey,返回與其關聯的cancelCtx。backgroundCtx和todoCtx:通常是沒有攜帶任何額外值的特殊類型的Context,遇到這兩種類型時直接返回nil。- 如果是未知類型,則繼續調用
Value方法尋找。
小結
這幾個 context 的核心就是cancelCtx,通過它來傳播取消信號,其它的 context 類型以及一些第三方的 context 類型也都是一層套一層,在此基礎上實現各式各樣的功能。
