Go 語言 context 包實踐

FunTester發表於2024-07-24

引子

Java 語言當中,特別是在 Spring 語境下,通常我們會遇到處理上下文的需求。一般場景中,我們可以利用 java.lang.ThreadLocal 來實現,基於執行緒維度對變數進行管理。ThreadLocal 執行緒記憶體儲和訪問變數的機制,非常適合在單個請求的生命週期內傳遞上下文資訊。

下面是個簡單的請求上下文的例子:

public class RequestContext {
    private static final ThreadLocal<RequestContext> threadLocal = ThreadLocal.withInitial(RequestContext::new);

    private String userId;
    private String requestId;

    public static RequestContext getCurrent() {
        return threadLocal.get();
    }

    public static void setCurrent(RequestContext context) {
        threadLocal.set(context);
    }

    public static void clear() {
        threadLocal.remove();
    }

    // Getters and setters
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
}

使用的話,可以在攔截器中實現初始化賦值或者清楚資料。PS:請注意執行緒安全的問題和遵守 java.lang.ThreadLocal 最佳實現,切莫自創用法。

Go 語言中,基於 goroutine 進行上下文管理的就是本文的主角 context 包。

簡介

Go 語言的 context 包是在 Go 1.7 版本引入的,用於在不同的 goroutine 之間傳遞請求範圍內的值、取消訊號和截止日期。它在處理併發操作時非常有用,可以透過 context 物件來控制和管理 goroutine 的生命週期。

context 包的核心型別是 Context 介面,它包含四個方法:DeadlineDoneErrValueDeadline 方法返回操作的截止時間,Done 方法返回一個通道,當操作應該取消時,該通道會關閉,Err 方法返回取消的錯誤原因,Value 方法允許儲存和檢索鍵值對。

在實際使用中,context 包常用於網路請求、資料庫操作和其他需要取消和超時控制的操作。透過在函式間傳遞 Context 物件,可以實現更靈活和可控的併發操作,避免 goroutine 洩漏和資源浪費。

建立方法

Background

在 Go 語言的 context 包中,context.Background() 用於返回一個空的上下文,它通常作為根上下文使用。這個根上下文在整個程式生命週期記憶體在,永遠不會被取消或超時。context.Background() 常用於初始化傳遞給其他上下文的頂層上下文,例如在啟動伺服器或處理請求時使用。

ctx := context.Background()

TODO

在 Go 語言的 context 包中,context.TODO() 返回一個空的上下文,它與 context.Background() 相似,但其主要用途是作為佔位符。通常在程式碼尚未確定具體上下文需求時使用 context.TODO(),以便稍後替換為適當的上下文。

ctx := context.TODO()

WithCancel

在 Go 語言的 context 包中,context.WithCancel 返回一個可取消的上下文及其取消函式。這個函式用於建立一個新的上下文,當呼叫返回的取消函式時,該上下文及其所有子上下文都會被取消。

ctx, cancel := context.WithCancel(context.Background())  
defer cancel()

WithDeadline

在 Go 語言的 context 包中,context.WithDeadline 返回一個上下文,該上下文會在指定的時間點自動取消。這種方式對於需要在特定時間點之前完成操作的場景非常有用。

// 建立一個根上下文  
rootCtx := context.Background()  
// 設定一個未來的時間點  
deadline := time.Now().Add(3 * time.Second)  
// 基於根上下文建立一個具有截止時間的子上下文  
_, cancel := context.WithDeadline(rootCtx, deadline)  
cancel() // 確保在操作完成後取消上下文

WithTimeout

在 Go 語言的 context 包中,context.WithTimeout 是一個非常常用的函式,它建立一個帶有超時的上下文。與 context.WithDeadline 類似,context.WithTimeout 會在指定的時間段後自動取消上下文。這對於需要在限定時間內完成的任務非常有用。

// 建立一個根上下文  
rootCtx := context.Background()  
// 基於根上下文建立一個具有 2 秒超時的子上下文  
_, cancel := context.WithTimeout(rootCtx, 2*time.Second)  
defer cancel() // 確保在操作完成後取消上下文

WithValue

在 Go 語言的 context 包中,context.WithValue 用於建立一個新的上下文,該上下文攜帶了特定的鍵值對。這個功能允許在上下文中傳遞請求範圍內的特定資料,如使用者認證資訊、配置選項等。與 context.WithCancel 和 context.WithTimeout 不同,context.WithValue 主要用於儲存和傳遞資料,而不是控制上下文的生命週期。

// 建立根上下文  
rootCtx := context.Background()  
// 使用 WithValue 建立一個新的上下文,並傳遞使用者資訊  
ctx := context.WithValue(rootCtx, "user", "FunTester")  
// 從上下文中檢索使用者資訊  
user, ok := ctx.Value("user").(string)  
if ok {  
    fmt.Println("使用者:", user)  
}

常用方法

Deadline

在 Go 語言中,context 包提供了 Deadline 方法,用於獲取上下文的截止時間。這在使用 context.WithDeadline 或 context.WithTimeout 建立的上下文時特別有用。

// 建立一個根上下文  
rootCtx := context.Background()  

// 設定一個 3 秒後的截止時間  
deadline := time.Now().Add(3 * time.Second)  
ctx, cancel := context.WithDeadline(rootCtx, deadline)  
defer cancel() // 確保在操作完成後取消上下文  
// 檢索截止時間  
d, ok := ctx.Deadline()  
if ok {  
    fmt.Println("截止時間:", d.Format("2006-01-02 15:04:05"))  
} else {  
    fmt.Println("沒有設定截止時間")  
}

Done

在 Go 語言的 context 包中,Done 方法是用於獲取上下文的取消訊號通道。當上下文被取消時,Done 方法返回的通道會接收到一個訊號。這對於處理超時、取消操作和清理工作非常重要。

package main  

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

func main() {  
    // 基於根上下文建立一個具有 2 秒超時的上下文  
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)  
    defer cancel() // 確保在操作完成後取消上下文  
    // 啟動一個 goroutine 執行一些工作  
    go func(ctx context.Context) {  
       for {  
          select {  
          case <-time.After(1 * time.Second):  
             fmt.Println("任務進行中")  
          case <-ctx.Done():  
             fmt.Println("任務被取消:", ctx.Err())  
             return  
          }  
       }  
    }(ctx)  
    // 等待 3 秒鐘,以觀察超時是否生效  
    time.Sleep(3 * time.Second)  
}

Err

在 Go 語言的 context 包中,Err 方法用於獲取上下文取消的錯誤資訊。它返回一個錯誤值,指示上下文的取消原因。這對於確定任務是否因超時、手動取消或其他原因終止非常有用。

package main  

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

func main() {  
    // 建立一個具有 2 秒超時的子上下文  
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)  
    defer cancel() // 確保在操作完成後取消上下文  

    // 啟動一個 goroutine 執行一些工作  
    go func(ctx context.Context) {  
       for {  
          select {  
          case <-time.After(1 * time.Second):  
             fmt.Println("任務進行中")  
          case <-ctx.Done():  
             // 使用 Err 方法獲取取消原因  
             err := ctx.Err()  
             if err != nil {  
                fmt.Println("任務被取消:", err)  
             }  
             return  
          }  
       }  
    }(ctx)  
    // 等待 3 秒鐘,以觀察超時是否生效  
    time.Sleep(3 * time.Second)  
    fmt.Println("程式結束")  
}

Value

在 Go 語言的 context 包中,Value 方法用於從上下文中檢索儲存的資料。Value 方法允許你在上下文中儲存和檢索特定的鍵值對,這對於在上下文中傳遞請求範圍的資料非常有用。

這個例子在之前 WithValue 中已經用到了,這裡不再重複。

併發中的應用

goroutine 的取消

在使用 Go 語言進行併發程式設計時,context包提供了一種優雅的方式來控制 goroutine 的生命週期。透過context.WithCancel函式,我們可以建立一個新的 context 例項,該例項可以被取消。

  • 當主 goroutine 決定不再需要某個操作繼續執行時,可以呼叫 context 的cancel函式。
  • 所有使用該 context 的 goroutine 都可以透過監聽 context 的Done()通道來感知到取消訊號,並做出相應的清理工作,然後退出。

例如,以下程式碼展示瞭如何使用context來控制兩個 goroutine 的取消:

package main  

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

func main() {  
    ctx, cancel := context.WithCancel(context.Background())  
    go func() {  
       for {  
          select {  
          case <-ctx.Done():  
             // 處理取消邏輯  
             fmt.Println("goroutine exit, cancel done")  
             return  
          default:  
             // 執行常規任務  
          }  
       }  
    }()  
    // 主goroutine可以在任何時候呼叫cancel來停止上述goroutine  
    time.Sleep(1 * time.Second)  
    cancel()  
}

4.2 超時控制

context包同樣支援超時控制,這在很多場景下非常有用,比如 API 呼叫、資料庫訪問等操作。透過context.WithTimeout函式,我們可以為 context 設定一個超時時間。

  • 當設定的超時時間到達後,context 會自動被取消,所有監聽Done()通道的 goroutine 都會收到通知。
  • 這種方式可以防止程式因為某個長時間執行的操作而卡住。

以下示例演示瞭如何使用context來進行超時控制:

package main  

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

func main() {  
    ctx, cancel := context.WithCancel(context.Background())  
    go func() {  
       for {  
          select {  
          case <-ctx.Done():  
             // 處理取消邏輯  
             fmt.Println("goroutine exit, cancel done")  
             return  
          default:  
             // 執行常規任務  
          }  
       }  
    }()  
    // 主goroutine可以在任何時候呼叫cancel來停止上述goroutine  
    time.Sleep(1 * time.Second)  
    cancel()  
}

在 Go 語言中,context包的設計初衷是為了簡化併發程式設計中的一些常見問題,比如在多個goroutine之間傳遞請求範圍的資料、處理超時和取消訊號等。它透過提供一個可以被傳遞給多個函式的請求上下文,使得程式碼更加清晰和易於管理。

在網路程式設計中的應用

在 Go 語言中,context包是處理併發請求時不可或缺的工具,尤其是在網路程式設計中。它允許開發者傳遞請求範圍的值、取消訊號和截止時間,從而實現對 HTTP 請求的精細控制。

  • 請求取消:在處理 HTTP 請求時,客戶端可能會取消請求。透過context,我們可以檢測到這種取消訊號,並及時終止正在執行的請求處理邏輯,避免資源浪費。
  • 超時控制:網路請求往往需要設定超時,以避免伺服器資源被長時間佔用。利用context的超時功能,我們可以為每個請求設定合理的超時時間,提高服務的響應性和健壯性。
  • 請求資料傳遞:在處理複雜的 HTTP 請求時,我們可能需要在不同的處理階段傳遞額外的資料。context提供了一種機制,允許我們將這些資料儲存在請求的上下文中,方便跨階段訪問。

以下是context在 HTTP 請求中使用的具體示例:

package main  

import (  
    "context"  
    "fmt"
    "io"    
    "net/http"    
    "time"
    )  

func main() {  
    client := http.Client{  
       Timeout: time.Second * 10, // 設定客戶端超時時間  
    }  
    // 建立一個帶有超時的context  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)  
    defer cancel()  
    // 建立一個HTTP請求  
    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)  
    // 傳送請求  
    resp, _ := client.Do(req)  
    defer resp.Body.Close()  
    // 讀取響應體  
    body, _ := io.ReadAll(resp.Body)  
    fmt.Println("Response body:", string(body))  
}

在這個示例中,我們首先建立了一個http.Client例項,並設定了超時時間。然後,我們使用 context.WithTimeout 建立了一個帶有超時的 context,這個 context 被用於建立和傳送 HTTP 請求。如果請求在超時時間內沒有完成,context會觸發取消訊號,導致請求被中斷。透過這種方式,context包在網路程式設計中的應用可以顯著提高 HTTP 請求處理的靈活性和效率。

  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章