天天看點

go語言上下文Context講解

作者:幹飯人小羽
go語言上下文Context講解
Go version → 1.20.4

前言

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

Go 在 1.7 引入了 context 包,目的是為了在不同的 goroutine 之間或跨 API 邊界傳遞逾時、取消信号和其他請求範圍内的值(與該請求相關的值。這些值可能包括使用者身份資訊、請求處理日志、跟蹤資訊等等)。

在 Go 的日常開發中,Context 上下文對象無處不在,無論是處理網絡請求、資料庫操作還是調用 RPC 等場景下,都會使用到 Context。那麼,你真的了解它嗎?熟悉它的正确用法嗎?了解它的使用注意事項嗎?喝一杯你最喜歡的飲料,随着本文一探究竟吧。

Context 接口

context 包在提供了一個用于跨 API 邊界傳遞逾時、取消信号和其他請求範圍值的通用資料結構。它定義了一個名為 Context 的接口,該接口包含一些方法,用于在多個 Goroutine 和函數之間傳遞請求範圍内的資訊。

以下是 Context 接口的定義:

go複制代碼type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key any) any
}
           

Context 的核心方法

go語言上下文Context講解

Context 接口中有四個核心方法:Deadline()、Done()、Err()、Value()。

Deadline()

Deadline() (deadline time.Time, ok bool) 方法傳回 Context 的截止時間,表示在這個時間點之後,Context 會被自動取消。如果 Context 沒有設定截止時間,該方法傳回一個零值 time.Time 和一個布爾值 false。

go複制代碼deadline, ok := ctx.Deadline()
if ok {
    // Context 有截止時間
} else {
    // Context 沒有截止時間
}
           

Done()

Done() 方法傳回一個隻讀通道,當 Context 被取消時,該通道會被關閉。你可以通過監聽這個通道來檢測 Context 是否被取消。如果 Context 永不取消,則傳回 nil。

go複制代碼select {
case <-ctx.Done():
    // Context 已取消
default:
    // Context 尚未取消
}
           

Err()

Err() 方法傳回一個 error 值,表示 Context 被取消時産生的錯誤。如果 Context 尚未取消,該方法傳回 nil。

go複制代碼if err := ctx.Err(); err != nil {
    // Context 已取消,處理錯誤
}
           

Value()

Value(key any) any 方法傳回與 Context 關聯的鍵值對,一般用于在 Goroutine 之間傳遞請求範圍内的資訊。如果沒有關聯的值,則傳回 nil。

go複制代碼value := ctx.Value(key)
if value != nil {
    // 存在關聯的值
}
           

Context 的建立方式

go語言上下文Context講解

context.Background()

context.Background() 函數傳回一個非 nil 的空 Context,它沒有攜帶任何的值,也沒有取消和逾時信号。通常作為根 Context 使用。

go複制代碼ctx := context.Background()
           

context.TODO()

context.TODO() 函數傳回一個非 nil 的空 Context,它沒有攜帶任何的值,也沒有取消和逾時信号。雖然它的傳回結果和 context.Background() 函數一樣,但是它們的使用場景是不一樣的,如果不确定使用哪個上下文時,可以使用 context.TODO()。

go複制代碼ctx := context.TODO()
           

context.WithValue()

context.WithValue(parent Context, key, val any) 函數接收一個父 Context 和一個鍵值對 key、val,傳回一個新的子 Context,并在其中添加一個 key-value 資料對。

go複制代碼ctx := context.WithValue(parentCtx, "username", "陳明勇")
           

context.WithCancel()

context.WithCancel(parent Context) (ctx Context, cancel CancelFunc) 函數接收一個父 Context,傳回一個新的子 Context 和一個取消函數,當取消函數被調用時,子 Context 會被取消,同時會向子 Context 關聯的 Done() 通道發送取消信号,屆時其衍生的子孫 Context 都會被取消。這個函數适用于手動取消操作的場景。

go複制代碼ctx, cancelFunc := context.WithCancel(parentCtx)  
defer cancelFunc()
           

context.WithCancelCause() 與 context.Cause()

context.WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) 函數是 Go 1.20 版本才新增的,其功能類似于 context.WithCancel(),但是它可以設定額外的取消原因,也就是 error 資訊,傳回的 cancel 函數被調用時,需傳入一個 error 參數。

go複制代碼ctx, cancelFunc := context.WithCancelCause(parentCtx)
defer cancelFunc(errors.New("原因"))
           

context.Cause(c Context) error 函數用于傳回取消 Context 的原因,即錯誤值 error。如果是通過 context.WithCancelCause() 函數傳回的取消函數 cancelFunc(myErr) 進行的取消操作,我們可以擷取到 myErr 的值。否則,我們将得到與 c.Err() 相同的傳回值。如果 Context 尚未被取消,将傳回 nil。

go複制代碼err := context.Cause(ctx)
           

context.WithDeadline()

context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc) 函數接收一個父 Context 和一個截止時間作為參數,傳回一個新的子 Context。當截止時間到達時,子 Context 其衍生的子孫 Context 會被自動取消。這個函數适用于需要在特定時間點取消操作的場景。

go複制代碼deadline := time.Now().Add(time.Second * 2)
ctx, cancelFunc := context.WithTimeout(parentCtx, deadline)
defer cancelFunc()
           

context.WithTimeout()

context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 函數和 context.WithDeadline() 函數的功能是一樣的,其底層會調用 WithDeadline() 函數,隻不過其第二個參數接收的是一個逾時時間,而不是截止時間。這個函數适用于需要在一段時間後取消操作的場景。

go複制代碼ctx, cancelFunc := context.WithTimeout(parentCtx, time.Second * 2)
defer cancelFunc()
           

Context 的使用場景

傳遞共享資料

編寫中間件函數,用于向 HTTP 處理鍊中添加處理請求 ID 的功能。

go複制代碼type key int

const (
   requestIDKey key = iota
)

func WithRequestId(next http.Handler) http.Handler {
   return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
      // 從請求中提取請求ID和使用者資訊
      requestID := req.Header.Get("X-Request-ID")

      // 建立子 context,并添加一個請求 Id 的資訊
      ctx := context.WithValue(req.Context(), requestIDKey, requestID)

      // 建立一個新的請求,設定新 ctx
      req = req.WithContext(ctx)

      // 将帶有請求 ID 的上下文傳遞給下一個處理器
      next.ServeHTTP(rw, req)
   })
}
           

首先,我們從請求的頭部中提取請求 ID。然後使用 context.WithValue 建立一個子上下文,并将請求 ID 作為鍵值對存儲在子上下文中。接着,我們建立一個新的請求對象,并将子上下文設定為新請求的上下文。最後,我們将帶有請求 ID 的上下文傳遞給下一個處理器。 這樣,通過使用 WithRequestId 中間件函數,我們可以在處理請求的過程中友善地擷取和使用請求 ID,例如在 日志記錄、跟蹤和調試等方面。

傳遞取消信号,結束任務

啟動一個工作協程,接收到取消信号就停止工作。

go複制代碼package main

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

func main() {
   ctx, cancelFunc := context.WithCancel(context.Background())
   go Working(ctx)

   time.Sleep(3 * time.Second)
   cancelFunc()

   // 等待一段時間,以確定工作協程接收到取消信号并退出
   time.Sleep(1 * time.Second)
}

func Working(ctx context.Context) {
   for {
      select {
      case <-ctx.Done():
         fmt.Println("下班啦...")
         return
      default:
         fmt.Println("陳明勇正在工作中...")
      }
   }
}
           

執行結果

erlang複制代碼······
······
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
下班啦...
           

在上面的示例中,我們建立了一個 Working 函數,它會不斷執行工作任務。我們使用 context.WithCancel 建立了一個上下文 ctx 和一個取消函數 cancelFunc。然後,啟動了一個工作協程,并将上下文傳遞給它。

在主函數中,需要等待一段時間(3 秒)模拟業務邏輯的執行。然後,調用取消函數 cancelFunc,通知工作協程停止工作。工作協程在每次循環中都會檢查上下文的狀态,一旦接收到取消信号,就會退出循環。

最後,等待一段時間(1 秒),以確定工作協程接收到取消信号并退出。

逾時控制

模拟耗時操作,逾時控制。

go複制代碼package main

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

func main() {
   // 使用 WithTimeout 建立一個帶有逾時的上下文對象
   ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
   defer cancel()

   // 在另一個 goroutine 中執行耗時操作
   go func() {
      // 模拟一個耗時的操作,例如資料庫查詢
      time.Sleep(5 * time.Second)
      cancel()
   }()

   select {
   case <-ctx.Done():
      fmt.Println("操作已逾時")
   case <-time.After(10 * time.Second):
      fmt.Println("操作完成")
   }
}
           

執行結果

複制代碼操作已逾時
           

在上面的例子中,首先使用 context.WithTimeout() 建立了一個帶有 3 秒逾時的上下文對象 ctx, cancel := context.WithTimeout(ctx, 3*time.Second)。

接下來,在一個新的 goroutine 中執行一個模拟的耗時操作,例如等待 5 秒鐘。當耗時操作完成後,調用 cancel() 方法來取消逾時上下文。

最後,在主 goroutine 中使用 select 語句等待逾時上下文的完成信号。如果在 3 秒内耗時操作完成,那麼會輸出 "操作完成"。如果超過了 3 秒仍未完成,逾時上下文的 Done() 通道會被關閉,輸出 "操作已逾時"。

使用 Context 的一些規則

使用 Context 上下文,應該遵循以下規則,以保持包之間的接口一緻,并使靜态分析工具能夠檢查上下文傳播:

  • 不要在結構類型中加入 Context 參數,而是将它顯式地傳遞給需要它的每個函數,并且它應該是第一個參數,通常命名為 ctx:
  • go複制代碼
  • func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
  • 即使函數允許,也不要傳遞 nil Context。如果不确定要使用哪個 Context,建議使用 context.TODO()。
  • 僅将 Context 的值用于傳輸程序和 api 的請求作用域資料,不能用于向函數傳遞可選參數。[1]

小結

本文詳細介紹了 Go 語言中的 Context 上下文,通過閱讀本文,相信你們對 Context 的功能和使用場景有所了解。同時,你們也應該能夠根據實際需求選擇最合适的 Context 建立方式,并且根據規則,正确、高效地使用它。

繼續閱讀