Go 語言基礎之 Context 詳解

程式設計師祝融發表於2023-05-10

之前有兄弟留言想學習一下 Context,他來了,雖遲但到。

前言

在 Go 語言中,Context 是一個非常重要的概念,它用於在不同的 goroutine 之間傳遞請求域的相關資料,並且可以用來控制 goroutine 的生命週期和取消操作。本文將深入探討 Go 語言中 Context 特性 和 Context 的高階使用方法。

基本用法

在 Go 語言中,Context 被定義為一個介面型別,它包含了三個方法:

# go version 1.18.10
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
      Value(key any) any
}
  • Deadline() 方法用於獲取 Context 的截止時間,
  • Done() 方法用於返回一個只讀的 channel,用於通知當前 Context 是否已經被取消,
  • Err() 方法用於獲取 Context 取消的原因,
  • Value() 方法用於獲取 Context 中儲存的鍵值對資料。

我們日常編寫程式碼時,Context 物件會被被約定作為函式的第一個引數傳遞,eg:

func users(ctx context.Context, request *Request) {
    // ... code
}

在函式中,可以透過 ctx 引數來獲取相關的 Context 資料,舉個超時的 eg:

deadline, ok := ctx.Deadline()
if ok && deadline.Before(time.Now()) {
    // 超時
    return
}

Context 控制 goroutine 的生命週期

在 Go 語言中,goroutine 是一種非常常見的併發程式設計模型,而 Context 可以被用來控制 goroutine 的生命週期,從而避免出現 goroutine 洩漏或者不必要的等待操作。

eg,看一下下方程式碼:

func users(ctx context.Context, req *Request) {
    // 啟動一個 goroutine 來處理請求
    go func() {
        // 處理請求...
    }()
}

上面的程式碼中,我們啟動了一個 goroutine 來處理請求,但是沒有任何方式來控制這個 goroutine 的生命週期,如果這個請求被取消了,那麼這個 goroutine 就會一直存在,直到它完成為止。為了避免這種情況的發生,我們可以使用 Context 來控制 goroutine 的生命週期,eg:

func users(ctx context.Context, req *Request) {
    // 啟動一個 goroutine 來處理請求
    go func(ctx context.Context) {
        // 處理請求...
    }(ctx)
}

在上面的程式碼中,我們將 Context 物件作為引數傳遞給了 goroutine 函式,這樣在請求被取消時,goroutine 就可以及時退出。

使用 WithValue() 傳遞資料

除了用於控制 goroutine 的生命週期,Context 還可以被用來在不同的 goroutine 之間傳遞請求域的相關資料。為了實現這個目的,我們可以使用 Context 的 WithValue() 方法,eg:

type key int

const (
    userKey key = iota
)

func users(ctx context.Context, req *Request) {
    // 從請求中獲取使用者資訊
    user := req.GetUser
    // 將使用者資訊儲存到 Context 中
    ctx = context.WithValue(ctx, userKey, user)
    
    // 啟動一個 goroutine 來處理請求
    go func(ctx context.Context) {
        // 從 Context 中獲取使用者資訊
        user := ctx.Value(userKey).(*User)
    
        // 處理請求...
    }(ctx)

}
    
    

在上面的程式碼中,我們定義了一個 key型別的常量 userKey,然後在 users()函式中將使用者資訊儲存到了 Context 中,並將 Context 物件傳遞給了 goroutine 函式。

在 goroutine 函式中,我們使用 ctx.Value()方法來獲取 Context 中儲存的使用者資訊。

注:

Context 中儲存的鍵值對資料應該是執行緒安全的,因為它們可能會在多個 goroutine 中同時訪問。

使用 WithCancel() 取消操作

除了控制 goroutine 的生命週期和傳遞資料之外,Context 還可以被用來執行取消操作。為了實現這個目的,我們可以使用 Context 的 WithCancel()方法,eg:

func users(ctx context.Context, req *Request) {
    // 建立一個可以取消的 Context 物件
    ctx, cancel := context.WithCancel(ctx)

    // 啟動一個 goroutine 來處理請求
    go func(ctx context.Context) {
        // 等待請求完成或者被取消
        select {
        case <-time.After(time.Second):
            // 請求完成
            fmt.Println("Request finish")
        case <-ctx.Done():
            // 請求被取消
            fmt.Println("Request canceled")
        }
    }(ctx)

    // 等待一段時間後取消請求
    time.Sleep(time.Millisecond * 800)
    cancel()
}

在上面的程式碼中,我們使用 WithCancel() 方法建立了一個可以取消的 Context 物件,並將取消操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者被取消,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。

使用 WithDeadline() 設定截止時間

除了使用 WithCancel() 方法進行取消操作之外,Context 還可以被用來設定截止時間,以便在超時的情況下取消請求。為了實現這個目的,我們可以使用 Context 的 WithDeadline() 方法,eg:

func users(ctx context.Context, req *Request) {
    // 設定請求的截止時間為當前時間加上 1 秒鐘
    ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second))

    // 啟動一個 goroutine 來處理請求
    go func(ctx context.Context) {
        // 等待請求完成或者超時
        select {
            case <-time.After(time.Millisecond * 500):
            // 請求完成
            fmt.Println("Request finish")
            case <-ctx.Done():
            // 請求超時或者被取消
            fmt.Println("Request canceled or timed out")
        }
    }(ctx)

    // 等待一段時間後取消請求
    time.Sleep(time.Millisecond * 1500)
    cancel()
}

在上面的程式碼中,我們使用 WithDeadline() 方法設定了一個截止時間為當前時間加上 1 秒鐘的 Context 物件,並將超時操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者超時,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。

注:

在使用 WithDeadline() 方法設定截止時間的時候,如果截止時間已經過期,則 Context 物件將被立即取消。

使用 WithTimeout() 設定超時時間

除了使用 WithDeadline() 方法進行截止時間設定之外,Context 還可以被用來設定超時時間。為了實現這個目的,我們可以使用 Context 的 WithTimeout() 方法,eg:

func users(ctx context.Context, req *Request) {
    // 設定請求的超時時間為 1 秒鐘
    ctx, cancel := context.WithTimeout(ctx, time.Second)

    // 啟動一個 goroutine 來處理請求
    go func(ctx context.Context) {
        // 等待請求完成或者超時
        select {
        case <-time.After(time.Millisecond * 500):
            // 請求完成
            fmt.Println("Request completed")
        case <-ctx.Done():
            // 請求超時或者被取消
            fmt.Println("Request canceled or timed out")
        }
    }(ctx)

    // 等待一段時間後取消請求
    time.Sleep(time.Millisecond * 1500)
    cancel()
}

在上面的程式碼中,我們使用 WithTimeout() 方法設定了一個超時時間為 1 秒鐘的 Context 物件,並將超時操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者超時,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。

注:

需要注意的是,在使用 WithTimeout() 方法設定超時時間的時候,如果超時時間已經過期,則 Context 物件將被立即取消。

Context 的傳遞

在一個應用程式中,不同的 goroutine 可能需要共享同一個 Context 物件。為了實現這個目的,Context 物件可以透過函式呼叫或者網路傳輸等方式進行傳遞。

例如,我們可以在一個處理 HTTP 請求的函式中建立一個 Context 物件,並將它作為引數傳遞給一個資料庫查詢函式,以便在查詢過程中使用這個 Context 物件進行取消操作。程式碼 eg:

func users(ctx context.Context, req *Request) {
    // 在處理 HTTP 請求的函式中建立 Context 物件
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()

    // 呼叫資料庫查詢函式並傳遞 Context 物件
    result, err := findUserByName(ctx, req)
    if err != nil {
        // 處理查詢錯誤...
    }

    // 處理查詢結果...
}

func findUserByName(ctx context.Context, req *Request) (*Result, error) {
    // 在資料庫查詢函式中使用傳遞的 Context 物件
    rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE name = ?", req.Name)
    if err != nil {
        // 處理查詢錯誤...
      }
      defer rows.Close()
      // 處理查詢結果...
}

在上面的程式碼中,我們在處理 HTTP 請求的函式中建立了一個 Context 物件,並將它作為引數傳遞給了一個資料庫查詢函式 findUserByName()。在 findUserByName() 函式中,我們使用傳遞的 Context 物件來呼叫 db.QueryContext() 方法進行查詢操作。由於傳遞的 Context 物件可能會在查詢過程中被取消,因此我們需要在查詢完成後檢查查詢操作的錯誤,以便進行相應的處理。

注:

在進行 Context 的傳遞時,我們需要保證傳遞的 Context 物件是原始 Context 物件的子 Context,以便在需要取消操作時能夠同時取消所有相關的 goroutine。如果傳遞的 Context 物件不是原始 Context 物件的子 Context,則取消操作只會影響到當前 goroutine,而無法取消其他相關的 goroutine。

總結

在 Go 語言中,Context 是一個非常重要的特性,包括其基本用法和一些高階用法。Context 可以用於管理 goroutine 的生命週期和取消操作,避免出現資源洩漏和死鎖等問題,同時也可以提高應用程式的效能和可維護性。

在使用 Context 的時候,需要注意以下幾點:

    1. 在建立 goroutine 時,需要將原始 Context 物件作為引數傳遞給它。
    1. 在 goroutine 中,需要使用傳遞的 Context 物件來進行取消操作,以便能夠及時釋放相關的資源。
    1. Context 的傳遞時,需要保證傳遞的 Context 物件是原始 Context 物件的子 Context,以便在需要取消操作時能夠同時取消所有相關的 goroutine。
    1. 在使用 WithCancel 和 WithTimeout 方法建立 Context 物件時,需要及時呼叫 cancel 函式,以便能夠及時釋放資源。
    1. 在一些場景下,可以使用 WithValue 方法將資料儲存到 Context 中,以便在不同的 goroutine 之間共享資料。

相關文章