一文搞懂 Go 標準庫 context 包

自從 context 包在 Go 1.7 版本 [1] 加入 Go 標準庫,它就成爲了 Go 標準庫中較難理解和易誤用的包之一。在我的博客中目前尚未有一篇系統介紹 context 包的文章,很多來自 Go 專欄 [2] 或《Go 語言精進之路》[3] 的讀者都希望我能寫一篇介紹 context 包的文章,今天我就來嘗試一下 ^_^。

1. context 包入標準庫歷程

2014 年,Go 團隊核心成員 Sameer Ajmani 在 Go 官博上發表了一篇文章 “Go Concurrency Patterns: Context”[4],介紹了 Google 內部設計和實現的一個名爲 context 的包以及該包在 Google 內部實踐後得出的一些應用模式。隨後,該包被開源並放在 golang.org/x/net/context 下維護。兩年後,也就是 2016 年,golang.org/x/net/context 包正式被挪入 Go 標準庫,這就是目前 Go 標準庫 context 包的誕生歷程。

歷史經驗告訴我們:但凡 Google 內部認爲是好東西的,基本上最後都進入到 Go 語言或標準庫當中了。context 包就是其中之一,後續 Go 1.9 版本加入的 type alias 語法 [5] 也印證了這一點。可以預測:即將於 Go 1.20 版本以實驗特性身份加入的 arena 包 [6] 離最終正式加入 Go 也只是時間問題了 ^_^!

2. context 包解決的是什麼問題?

正確定義問題比解決問題更重要。在 Sameer Ajmani 的文章中,他在一開篇就對引入 context 包要解決的問題做了明確的闡述:

在 Go 服務器中,每個傳入的請求都在自己的 goroutine 中處理。請求的處理程序經常啓動額外的 goroutine 來訪問後端服務,如數據庫和 RPC 服務。處理一個請求的一組 goroutine 通常需要訪問該請求相關的特定的值,比如最終用戶的身份、授權令牌和請求的 deadline 等。當一個請求被取消或處理超時時,所有在該請求上工作的 goroutines 應該迅速退出,以便系統可以回收他們正在使用的任何資源。

從這段描述中,我至少 get 到兩點:

後端服務程序有這樣的需求,即在處理某請求的函數 (Handler Function) 中調用其他函數時,** 傳遞與請求相關的(request-specific)、請求內容之外的值信息(以下稱之爲上下文中的值信息)**,如下圖所示:

我們看到:這種函數調用以及傳值可以發生在同一 goroutine 的函數之間 (比如上圖中的 Handler 函數調用 middleware 函數)、同一進程的多個 goroutine 之間 (如被調用函數創建了新的 goroutine),甚至是不同進程的 goroutine 之間 (比如 rpc 調用)。

同一 goroutine 下因處理外部請求 (request) 而發生函數調用時,如果被調用的函數 (callee) 並沒有啓動新 goroutine 或進行跨進程的處理(如 rpc 調用),這時更多的是在函數間傳值,即傳遞上下文中的值信息。

但當被調用的函數 (callee) 啓動新 goroutine 或進行跨進程處理時,這通常會是一種異步調用。爲什麼要啓動新 goroutine 進行異步調用呢?更多是爲了控制。如果是同步調用,一旦被調用方出現延遲或故障,這次調用很可能長期阻塞,調用者自身既無法消除這種影響,也不能及時回收掉處理這次請求所申請的各種資源,更無法保證服務接口之間的 SLA。

注意:調用者與被調用者之間可以是同步調用,也可以是異步調用,而被調用者則通常啓動新的 goroutine 來實現一種 “異步調用”。

那麼怎麼控制異步調用呢?這回我們在調用者與被調用者之間傳遞的不再是一種值信息,而是一種 “默契”,即一種控制機制,如下圖所示:

當被調用者在調用者的限定時間內完成任務,調用成功,被調用者釋放所有資源;當被調用者無法在限定時間內完成或被調用者收到調用者取消調用的通知時,也能結束調用並釋放資源。

接下來,我們就來看看 Go 標準庫 context 包是如何解決上述兩個問題的。

3. context 包的構成

Go 將對上面兩個問題 “傳值與控制” 的解決方案統一放到了 context 包下的一個名爲 Context 接口類型中了:

// $GOROOT/src/context/context.go
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

注:“上下文” 本沒有統一標準,很多第三方包也有自己 Context 的定義,但 Go 1.7 之後都逐漸轉爲使用 Go 標準庫的 context.Context 了。

如果你讀懂了前面 context 包要解決的問題,你大致也能將 Context 接口類型中的方法分爲兩類,第一類就是 Value 方法,用於解決 “傳值” 的問題;其他三個方法 (Deadline、Done 和 Err) 劃歸爲第二類,用於解決 “傳遞控制” 的問題。

如果僅僅是定義 Context 這樣一個接口類型,統一了對 Context 的抽象,那事情就未得到徹底解決 (但也比 log 包做的要好了 [7]),Go context 包 “好人做到底”,還提供了一系列便利的函數以及若干內置的 Context 接口的實現。下面我們逐一來看一下。

1) WithValue 函數

首先我們看一下用於傳值的 WithValue 函數。

// $GOROOT/src/context/context.go
func WithValue(parent Context, key, val any) Context

WithValue 函數基於 parent Context 創建一個新的 Context,這個新的 Context 既保存了一份 parent Context 的副本,同時也保存了 WithValue 函數接受的那個 key-val 對。WithValue 其實返回一個名爲 * valueCtx 類型的實例,*valueCtx 實現了 Context 接口,它由三個字段組成:

// $GOROOT/src/context/context.go

type valueCtx struct {
    Context
    key, val any
}

結合 WithValue 的實現邏輯,valueCtx 中的 Context 被賦值爲 parent Context,key 和 val 分別保存了 WithValue 傳入的 key 和 val。

在新 Context 創建成功後,處理函數後續將基於該新 Context 進行上下文中的值信息的傳遞,我們來看一個例子:

// github.com/bigwhite/experiments/tree/master/context-examples/with_value/main.go

package main

import (
    "context"
    "fmt"
)

func f3(ctx context.Context, req any) {
    fmt.Println(ctx.Value("key0"))
    fmt.Println(ctx.Value("key1"))
    fmt.Println(ctx.Value("key2"))
}

func f2(ctx context.Context, req any) {
    ctx2 := context.WithValue(ctx, "key2""value2")
    f3(ctx2, req)
}

func f1(ctx context.Context, req any) {
    ctx1 := context.WithValue(ctx, "key1""value1")
    f2(ctx1, req)
}

func handle(ctx context.Context, req any) {
    ctx0 := context.WithValue(ctx, "key0""value0")
    f1(ctx0, req)
}

func main() {
    rootCtx := context.Background()
    handle(rootCtx, "hello")
}

在上面這段代碼中,handle 是負責處理 “請求” 的入口函數,它接受一個由 main 函數創建的 root Context 以及請求內容本身("hello"),之後 handle 函數基於傳入的 ctx,通過 WithValue 函數創建了一個包含了自己附加的 key0-value0 對的新 Context,這個新 Context 將在調用 f1 函數時作爲上下文傳給 f1;依次類推,f1、f2 都基於傳入的 ctx 通過 WithValue 函數創建了包含自己附加的值信息的新 Context,在函數調用鏈的末端,f3 通過 Context 的 Value 方法從傳入的 ctx 中嘗試取出上下文中的各種值信息,我們用一幅示意圖來展示一下這個過程:

我們運行一下上述代碼看看結果:

$go run main.go
value0
value1
value2

我們看到,f3 不僅從上下文中取出了 f2 附加的 key2-value2,還可以取出 handle、f1 等函數附加的值信息。這得益於滿足 Context 接口的 * valueCtx 類型 “順藤摸瓜” 的實現:

// $GOROOT/src/context/context.go

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, 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 *timerCtx:
            if key == &cancelCtxKey {
                return &ctx.cancelCtx
            }
            c = ctx.Context
        case *emptyCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}

我們看到在 * valueCtx case 中,如果 key 與當前 ctx 的 key 不同,就會繼續沿着 parent Ctx 路徑繼續查找,直到找到爲止。

我們看到:WithValue 用起來不難,也好理解。不過由於每個 valueCtx 僅能保存一對 key-val,這樣即便在一個函數中添加多個值信息,其使用模式也必須是這樣的:

ctx1 := WithValue(parentCtx, key1, val1)
ctx2 := WithValue(ctx1, key2, val2)
ctx3 := WithValue(ctx2, key3, val3)
nextCall(ctx3, req)

而不能是

ctx1 := WithValue(parentCtx, key1, val1)
ctx1 = WithValue(parentCtx, key2, val2)
ctx1 = WithValue(parentCtx, key3, val3)
nextCall(ctx1, req)

否則 ctx1 中僅會保存最後一次的 key3-val3 的信息,而 key1、key2 都會被覆蓋掉。

valueCtx 的這種設計也導致了 Value 方法的查找 key 的效率不是很高,是個 O(n) 的查找。在一些對性能敏感的 Web 框架中,valueCtx 和 WithValue 可能難有用武之地。

在上面的例子中,我們說到了 root Context,下面簡單說一下 root Context 的構建。

2) root Context 構建

root Context,也稱爲 top-level Context,即最頂層的 Context,通常在 main 函數、初始化函數、請求處理的入口 (某個 Handle 函數) 中創建。Go 提供了兩種 root Context 的構建方法 Background 和 TODO:

// $GOROOT/src/context/context.go

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

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

我們看到,雖然標準庫提供了兩種 root Context 的創建方法,但它們本質是一樣的,底層都返回的是一個與程序同生命週期的 emptyCtx 類型的實例。有小夥伴可能會問:Go 所有代碼共享一個 root Context 會不會有問題呢?

答案是不會!因爲 root Context 啥 “實事” 也不做,就像 “英聯邦國王” 一樣,僅具有名義上的象徵意義,它既不會存儲上下文值信息,也不會攜帶上下文控制信息,整個生命週期內它都不會被改變。它只是作爲二級上下文 parent Context 的指向,真正具有 “功能” 作用的 Context 是類似於首相或總理的 second-level Context:

通常我們都會使用 Background() 函數構造 root Context,而按照 context 包 TODO 函數的註釋來看,TODO 僅在不清楚應該使用哪個 Context 的情況下臨時使用。

3) WithCancel 函數

WithCancel 函數爲上下文提供了第一種控制機制:可取消 (cancel),它也是整個 context 包控制機制的基礎。我們先直觀感受一下 WithCancel 的作用,下面是 Go context 包文檔 [8] 中的一個例子:

package main

import (
 "context"
 "fmt"
)

func main() {
 gen := func(ctx context.Context) <-chan int {
  dst := make(chan int)
  n := 1
  go func() {
   for {
    select {
    case <-ctx.Done():
     return // returning not to leak the goroutine
    case dst <- n:
     n++
    }
   }
  }()
  return dst
 }

 ctx, cancel := context.WithCancel(context.Background())
 defer cancel() // cancel when we are finished consuming integers

 for n := range gen(ctx) {
  fmt.Println(n)
  if n == 5 {
   break
  }
 }
}

在這個例子,main 函數通過 WithCancel 創建了一個具有可取消屬性的 Context 實例,然後在調用 gen 函數時傳入了該實例。WithCancel 函數除了返回一個具有可取消屬性的 Context 實例外,還返回了一個 cancelFunc,這個 cancelFunc 就是握在調用者手裏的那個 “按鈕”,一旦按下該“按鈕”,即調用者發出“取消” 信號,異步調用中啓動的 goroutine 就應該放下手頭工作,老老實實地退出。

就像上面這個示例一樣,main 函數將 cancel Context 傳給 gen 後,gen 函數啓動了一個新 goroutine 用於生成一組數列,而 main 函數則從 gen 返回的 channel 中讀取這些數列中的數。main 函數在讀完第 5 個數字後,按下了 “按鈕”,即調用了 cancel Function。這時那個生成數列的 goroutine 會監聽到 Done channel 有事件,然後完成 goroutine 的退出。

這就是前面說過的那種調用者和被調用者 (以及調用者創建的新 goroutine) 之間應具備的那種 “默契”,這種“默契” 要求兩者都要基於上下文按一定的 “套路” 進行處理,在這個例子中就體現在調用者適時調用 cancel Function,而 gen 啓動的 goroutine 要監聽可取消 Context 實例的 Done channel

並且通常,我們在創建完一個 cancel Context 後,立即會通過 defer 將 cancel Function 註冊到 deferred function stack 中去,以防止因未調用 cancel Function 導致的資源泄露!在這個例子中,如果不調用 cancel Function,gen 函數創建的那個 goroutine 就會一直運行,雖然它生成的數字已經不會再有其他 goroutine 消費。

相較於 WithValue 函數,WithCancel 的實現略複雜:

// $GOROOT/src/context/context.go

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

其複雜就複雜在 propagateCancel 這個調用上:

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:
    }

    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 {
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

propagateCancel 通過 parentCancelCtx 向上順着 parent 路徑查找,之所以可以這樣,是因爲 Value 方法具備沿着 parent 路徑查找的特性:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) // 沿着parent路徑查找第一個cancelCtx
    if !ok {
        return nil, false
    }
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nil, false
    }
    return p, true
}

如果找到一個 cancelCtx,就將自己加入到該 cancelCtx 的 child map 中:

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
}

注:接口類型值是支持比較的,如果兩個接口類型值的動態類型相同且動態類型的值相同,那麼兩個接口類型值就相同。這也是 children 這個 map 用 canceler 接口作爲 key 的原因。

這樣當其 parent cancelCtx 的 cancel Function 被調用時,cancel function 會調用 cancelCtx 的 cancel 方法,cancel 方法會遍歷所有 children cancelCtx,然後調用 child 的 cancel 方法以達到關聯取消的目的,同時該 parent cancelCtx 會與所有 children cancelCtx 解除關係!

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
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    for child := range c.children { // 遍歷children,調用cancel方法
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil // 解除與children的關係
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

我們用一個例子來演示一下:

// github.com/bigwhite/experiments/tree/master/context-examples/with_cancel/cancelctx_map.go

package main

import (
 "context"
 "fmt"
 "time"
)

// 直接使用parent cancelCtx
func f1(ctx context.Context) {
 go func() {
  select {
  case <-ctx.Done():
   fmt.Println("goroutine created by f1 exit")
  }
 }()
}

// 基於parent cancelCtx創建新的cancelCtx
func f2(ctx context.Context) {
 ctx1, _ := context.WithCancel(ctx)
 go func() {
  select {
  case <-ctx1.Done():
   fmt.Println("goroutine created by f2 exit")
  }
 }()
}

// 使用基於parent cancelCtx創建的valueCtx
func f3(ctx context.Context) {
 ctx1 := context.WithValue(ctx, "key3""value3")
 go func() {
  select {
  case <-ctx1.Done():
   fmt.Println("goroutine created by f3 exit")
  }
 }()
}

// 基於parent cancelCtx創建的valueCtx之上創建cancelCtx
func f4(ctx context.Context) {
 ctx1 := context.WithValue(ctx, "key4""value4")
 ctx2, _ := context.WithCancel(ctx1)
 go func() {
  select {
  case <-ctx2.Done():
   fmt.Println("goroutine created by f4 exit")
  }
 }()
}

func main() {
 valueCtx := context.WithValue(context.Background()"key0""value0")
 cancelCtx, cf := context.WithCancel(valueCtx)
 f1(cancelCtx)
 f2(cancelCtx)
 f3(cancelCtx)
 f4(cancelCtx)

 time.Sleep(3 * time.Second)
 fmt.Println("cancel all by main")
 cf()
 time.Sleep(10 * time.Second) // wait for log output
}

上面這個示例演示了四種情況:

運行這個示例,我們得到:

cancel all by main
goroutine created by f1 exit
goroutine created by f2 exit
goroutine created by f3 exit
goroutine created by f4 exit

我們看到,無論是直接使用 parent cancelCtx,還是使用基於 parent cancelCtx 創建的其他各種 Ctx,當 parent cancelCtx 的 cancel Function 被調用後,所有監聽對應 child Done channel 的 goroutine 都能正確收到通知並退出。

當然這種 “取消通知” 只能由 parent 通知到下面的 children,反過來則不行,parent cancelCtx 不會因爲 child Context 的 cancel function 被調用而被 cancel 掉。另外如果某個 children cancelCtx 的 cancel Function 被調用後,該 children 會與其 parent cancelCtx 解綁。

在前面貼出的 propagateCancel 函數的實現中,我們還看到了另外一個分支,即 parentCancelCtx 函數返回的 ok 爲 false 時,propagateCancel 函數會啓動一個新的 goroutine 監聽 parent Done channel 和自身的 Done channel。什麼情況下會走到這個執行分支下呢?這種情況似乎不多!我們來看一個自定義 cancelCtx 的情況:

package main

import (
 "context"
 "fmt"
 "runtime"
 "time"
)

func f1(ctx context.Context) {
 ctx1, _ := context.WithCancel(ctx)
 go func() {
  select {
  case <-ctx1.Done():
   fmt.Println("goroutine created by f1 exit")
  }
 }()
}

type myCancelCtx struct {
 context.Context
 done chan struct{}
 err  error
}

func (ctx *myCancelCtx) Done() <-chan struct{} {
 return ctx.done
}

func (ctx *myCancelCtx) Err() error {
 return ctx.err
}

func WithMyCancelCtx(parent context.Context) (context.Context, context.CancelFunc) {
 var myCtx = &myCancelCtx{
  Context: parent,
  done:    make(chan struct{}),
 }

 return myCtx, func() {
  myCtx.done <- struct{}{}
  myCtx.err = context.Canceled
 }
}

func main() {
 valueCtx := context.WithValue(context.Background()"key0""value0")
 fmt.Println("before f1:", runtime.NumGoroutine())

 myCtx, mycf := WithMyCancelCtx(valueCtx)
 f1(myCtx)
 fmt.Println("after f1:", runtime.NumGoroutine())

 time.Sleep(3 * time.Second)
 mycf()
 time.Sleep(10 * time.Second) // wait for log output
}

在這個例子中,我們 “部分逃離” 了 context cancelCtx 的體系並自定義了一個實現了 Context 接口的 myCancelCtx,在這樣的情況下,當 f1 函數基於 myCancelCtx 構建自己的 child CancelCtx 時,由於向上找不到 * cancelCtx 類型,所以它 WithCancel 啓動了一個 goroutine 既監聽自己的 Done channel,也監聽其 parent Ctx(即 myCancelCtx)的 Done channel。

當 myCancelCtx 的 cancel Function 在 main 函數中被調用時 (mycf()),新建的 goroutine 會調用 child 的 cancel 函數實現操作取消。運行上面示例,我們得到如下結果:

$go run custom_cancelctx.go
before f1: 1
after f1: 3  // 在context包中新創建了一個goroutine
goroutine created by f1 exit

由此,我們看到,除了 “業務” 層面可能導致的資源泄露之外,cancel Context 的實現中也會有一些資源 (比如上面這個新建的 goroutine) 需要及時釋放,否則也會導致“泄露”。

一些小夥伴可能會問這樣一個問題:在被調用函數 (callee) 中,到底是繼續傳遞原 cancelCtx 給新建的 goroutine,還是基於 parent cancelCtx 創建一個新的 cancelCtx 再傳給 goroutine 用呢?這讓我想起了裝修時遇到的一個問題:是否在水管某些地方加閥門?

加上閥門,可以單獨控制一路的關閉!同樣在代碼中,基於 parent cancelCtx 創建新的 cancelCtx 可以做單獨取消操作,而不影響 parentCtx,這就看業務層代碼是否需要這麼做了。

到這裏,我們已經 get 到了 context 包提供的取消機制,但實際中,我們很難拿捏好 cancel Function 調用的時機。爲此,context 包提供了另外一個建構在 cancelCtx 之上的實用控制機制:timerCtx。接下來,我們就來看看 timerCtx。

4) WithDeadline 和 WithTimeout 函數

timerCtx 基於 cancelCtx 提供了一種基於 deadline 的取消控制機制:

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

context 包提供了兩個創建 timerCtx 的 API:WithDeadline 和 WithTimeout 函數:

// $GOROOT/src/context/context.go

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if 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)
    }
    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(false, 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) }
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

從實現來看,WithTimeout 就是 WithDeadline 的再包裝!我們弄懂 WithDeadline 即可。從 WithDeadline 的實現來看,該函數通過 time.AfterFunc 設置了一個定時器,定時器 fire 後的執行邏輯就是執行該 ctx 的 cancel Function。也就是說 timerCtx 既支持手工 cancel(原 cancelCtx 的機制),也支持定時 cancel,並且通常由定時器來完成 cancel。

有了 cancelCtx 的基礎,timerCtx 就不難理解了。不要要注意的一點時,即便有了定時器來 cancel 操作,我們也不要忘記顯式調用 WithDeadline 和 WithTimeout 返回的 cancel function,及早釋放資源不是更好麼!

4. 小結

本文對 Go 標準庫 context 包要解決的問題、context 包構成以及傳值和傳遞控制的原理做了簡要講解,相信讀完這些內容後,你再回頭去看你寫過的運用 context 包的代碼肯定會有更爲深刻的理解。

context 包目前在 Go 生態內得到廣泛應用,較爲典型的是在 http handler 中傳遞值信息、在 tracing 框架中通過在上下文中的 trace ID 來整合 tracing 信息等。

Go 社區對 context 包的聲音也不全是正面,其中 context.Context 具有 “病毒般” 的傳染性就是被集中詬病的方面。Go 官方也有一個 issue 記錄了 Go 社區對 context 包的反饋和優化建議 [9],有興趣的小夥伴可以去翻翻。

本文的 context 包源碼來自 Go 1.19.1 版本 [10],與老版本 Go 或 Go 的未來版本可能會有差別。

本文的源碼在這裏 [11] 可以下載。

5. 參考資料


“Gopher 部落” 知識星球 [12] 旨在打造一個精品 Go 學習和進階社羣!高品質首發 Go 技術文章,“三天” 首發閱讀權,每年兩期 Go 語言發展現狀分析,每天提前 1 小時閱讀到新鮮的 Gopher 日報,網課、技術專欄、圖書內容前瞻,六小時內必答保證等滿足你關於 Go 語言生態的所有需求!2022 年,Gopher 部落全面改版,將持續分享 Go 語言與 Go 應用領域的知識、技巧與實踐,並增加諸多互動形式。歡迎大家加入!

Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯繫方式:

商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。

參考資料

[1] 

Go 1.7 版本: https://tonybai.com/2016/06/21/some-changes-in-go-1-7

[2] 

Go 專欄: http://gk.link/a/10AVZ

[3] 

《Go 語言精進之路》: https://item.jd.com/13694000.html

[4] 

“Go Concurrency Patterns: Context”: https://go.dev/blog/context

[5] 

Go 1.9 版本加入的 type alias 語法: https://tonybai.com/2017/07/14/some-changes-in-go-1-9/

[6] 

arena 包: https://github.com/golang/go/issues/51317

[7] 

也比 log 包做的要好了: https://tonybai.com/2022/10/30/first-exploration-of-slog

[8] 

Go context 包文檔: https://pkg.go.dev/context

[9] 

Go 官方也有一個 issue 記錄了 Go 社區對 context 包的反饋和優化建議: https://github.com/golang/go/issues/28342

[10] 

Go 1.19.1 版本: https://tonybai.com/2022/08/22/some-changes-in-go-1-19

[11] 

這裏: https//github.com/bigwhite/experiments/tree/master/context-examples

[12] 

“Gopher 部落” 知識星球: https://wx.zsxq.com/dweb2/index/group/51284458844544

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xzNqpRbIvoJygSWeQJFjTw