Go 語言 Context 應用全攻略:異步編程利器

概述

在 Go 語言中,Context(上下文)是一個非常重要的概念,特別是在處理請求時。

允許在請求的整個生命週期內傳遞數據、控制請求的取消、處理超時等。

本文將介紹 Go 語言中 Context 的使用,幫助更好地理解與處理請求的傳遞與控制。

主要內容包括

  • Context 基礎

  • Context 創建與傳遞

  • Context 的超時與取消

  • Context 的鏈式操作

  • Context 在併發中的應用

  • Context 的應用場景

  • 最佳實踐與注意事項

1. Context 基礎

在 Go 語言中,context.Context 接口定義了一個請求的上下文。

它包含了請求的截止時間、取消信號和請求的數據。

使用 Context 可以在請求之間有效地傳遞數據,同時也可以控制請求的生命週期。

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

Context 接口包含了四個方法:Deadline() 返回 Context 的截止時間

Done() 返回一個通道,它會在 Context 被取消或超時時關閉

Err() 返回 Context 的錯誤信息,Value(key) 返回 Context 中與 key 關聯的值。

2. Context 創建與傳遞

2.1 創建和傳遞 Context

package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 創建一個帶有超時時間的Context,這裏設置超時時間爲2秒
  ctx, cancel := context.WithTimeout(rootContext, 2*time.Second)
  defer cancel()
  // 在新的goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
      fmt.Println("任務完成")
    case <-ctx.Done():
      fmt.Println("任務取消或超時")
    }
  }(ctx)
  // 等待一段時間,模擬程序運行
  time.Sleep(5 * time.Second)
}

在這個例子中,創建了一個帶有 2 秒超時時間的 Context,並在一個新的 goroutine 中執行一個任務。

在主 goroutine 中,等待了 5 秒,因此任務在超時之前完成,所以會輸出 "任務完成"。

2.2 使用 WithValue 傳遞數據

package main
import (
  "context"
  "fmt"
)
type key string
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 使用WithValue傳遞數據
  ctx := context.WithValue(rootContext, key("userID"), 123)
  // 在子函數中獲取傳遞的數據
  getUserID(ctx)
}
func getUserID(ctx context.Context) {
  // 從Context中獲取數據
  if userID, ok := ctx.Value(key("userID")).(int); ok {
    fmt.Println("UserID:", userID)
  } else {
    fmt.Println("UserID不存在")
  }
}

在這個示例中,使用 WithValue 方法在 Context 中傳遞了一個 userID 的值,並在 getUserID 函數中成功獲取並打印了這個值。

3. Context 的超時與取消

3.1 設置請求超時時間

package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 創建一個超時時間爲2秒的Context
  timeoutCtx, _ := context.WithTimeout(rootContext, 2*time.Second)
  // 創建一個手動取消的Context
  cancelCtx, cancel := context.WithCancel(rootContext)
  defer cancel()
  // 在新的goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
      fmt.Println("任務完成")
    case <-ctx.Done():
      fmt.Println("任務取消或超時")
    }
  }(timeoutCtx)
  // 在另一個goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(1 * time.Second):
      fmt.Println("另一個任務完成")
    case <-ctx.Done():
      fmt.Println("另一個任務取消")
    }
  }(cancelCtx)
  // 等待一段時間,模擬程序運行
  time.Sleep(5 * time.Second)
}

在上面例子中,用 WithTimeout 方法創建了一個帶有 2 秒超時時間的 Context。

在任務的 goroutine 中,用 select 語句監聽了超時和 Context 的取消兩個事件,以便及時響應。

3.2 處理請求取消

package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 創建一個可以手動取消的Context
  ctx, cancel := context.WithCancel(rootContext)
  defer cancel()
  // 在新的goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
      fmt.Println("任務完成")
    case <-ctx.Done():
      fmt.Println("任務取消")
    }
  }(ctx)
  // 等待一段時間,手動取消任務
  time.Sleep(2 * time.Second)
  cancel()
  // 等待一段時間,模擬程序運行
  time.Sleep(1 * time.Second)
}

在上面例子中,使用 WithCancel 方法創建了一個可以手動取消的 Context。

在主函數中,等待了 2 秒後,手動調用 cancel 函數取消了任務。

這時,在任務的 goroutine 中,ctx.Done() 會接收到取消信號,從而退出任務。

4. Context 的鏈式操作

在實際應用中,可能需要將多個 Context 串聯起來使用。

Go 語言的 Context 提供了 WithCancel、WithDeadline、WithTimeout 等方法。

可以用這些方法實現多個 Context 的協同工作。

package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 創建一個超時時間爲2秒的Context
  timeoutCtx, _ := context.WithTimeout(rootContext, 2*time.Second)
  // 創建一個手動取消的Context
  cancelCtx, cancel := context.WithCancel(rootContext)
  defer cancel()
  // 在新的goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(3 * time.Second):
      fmt.Println("任務完成")
    case <-ctx.Done():
      fmt.Println("任務取消或超時")
    }
  }(timeoutCtx)
  // 在另一個goroutine中執行任務
  go func(ctx context.Context) {
    select {
    case <-time.After(1 * time.Second):
      fmt.Println("另一個任務完成")
    case <-ctx.Done():
      fmt.Println("另一個任務取消")
    }
  }(cancelCtx)
  // 等待一段時間,模擬程序運行
  time.Sleep(5 * time.Second)
}

在示例中,創建了一個帶有 2 秒超時時間的 Context 和一個可以手動取消的 Context,然後分別傳遞給兩個不同的任務。

在主函數中,等待了 5 秒,超時時間爲 2 秒,因此第一個任務會因超時而取消,第二個任務則會在 1 秒後完成。

5. Context 在併發中的應用

5.1 使用 Context 控制多個協程

package main
import (
  "context"
  "fmt"
  "sync"
  "time"
)
func main() {
  // 創建一個根Context
  rootContext := context.Background()
  // 創建一個可以手動取消的Context
  ctx, cancel := context.WithCancel(rootContext)
  defer cancel()
  // 使用WaitGroup等待所有任務完成
  var wg sync.WaitGroup
  // 啓動多個協程執行任務
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
      defer wg.Done()
      select {
      case <-time.After(time.Duration(id) * time.Second):
        fmt.Println("任務", id, "完成")
      case <-ctx.Done():
        fmt.Println("任務", id, "取消")
      }
    }(i)
  }
  // 等待一段時間,然後手動取消任務
  time.Sleep(2 * time.Second)
  cancel()
  // 等待所有任務完成
  wg.Wait()
}

在上面例子中,創建了一個可以手動取消的 Context,並使用 sync.WaitGroup 等待所有任務完成。

在 for 循環中,啓動了 5 個協程,每個協程會等待一段時間後輸出任務完成信息。

在主函數中,程序等待了 2 秒後,手動調用 cancel 函數取消了任務,協程會接收到取消信號並退出。

5.2 避免 Context 濫用

在使用 Context 時,要避免將 Context 放在結構體中。

因爲 Context 應該作爲函數參數傳遞,而不應該被放在結構體中進行傳遞。

Context 應該限定在程序的最小作用域,不要傳遞到不需要它的函數中。

6. Context 的應用場景

6.1 HTTP 請求中的 Context 使用

package main
import (
  "fmt"
  "net/http"
  "time"
)
func handler(w http.ResponseWriter, r *http.Request) {
  ctx := r.Context()
  select {
  case <-time.After(2 * time.Second):
    fmt.Fprintln(w, "Hello, World!")
  case <-ctx.Done():
    err := ctx.Err()
    fmt.Println("Server:", err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}
func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

在上面示例中,創建了一個 HTTP 請求處理函數 handler。

在處理函數中,用 r.Context() 獲取到請求的 Context,並在其中執行一個耗時的任務。

如果請求超時,ctx.Done() 會接收到取消信號,可以在其中處理請求超時的邏輯。

6.2 數據庫操作中的 Context 使用

package main
import (
  "context"
  "database/sql"
  "fmt"
  "time"
  _ "github.com/go-sql-driver/mysql"
)
func main() {
  // 連接數據庫
  db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/database")
  if err != nil {
    fmt.Println("數據庫連接失敗:", err)
    return
  }
  defer db.Close()
  // 創建一個Context,設置超時時間爲5秒
  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  defer cancel()
  // 在Context的超時時間內執行數據庫查詢
  rows, err := db.QueryContext(ctx, "SELECT * FROM users")
  if err != nil {
    fmt.Println("數據庫查詢失敗:", err)
    return
  }
  defer rows.Close()
  // 處理查詢結果
  for rows.Next() {
    // 處理每一行數據
  }
}

在上面例子中,使用 database/sql 包進行數據庫查詢。創建了一個帶有 5 秒超時時間的 Context,並在其中執行數據庫查詢。

如果查詢時間超過 5 秒,Context 會接收到取消信號,可以在其中執行處理查詢超時的邏輯。

6.3 其他業務場景中的 Context 使用

在其他業務場景中,可使用 Context 實現更多複雜的任務協同。

例如,使用 Context 在多個微服務之間進行數據傳遞和超時控制。

以下是一個示例,演示瞭如何在微服務架構中使用 Context 進行跨服務的數據傳遞

package main
import (
  "context"
  "fmt"
  "time"
)
type Request struct {
  ID int
}
type Response struct {
  Message string
}
func microservice(ctx context.Context, reqCh chan Request, resCh chan Response) {
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Microservice shutting down...")
      return
    case req := <-reqCh:
      // 模擬處理請求的耗時操作
      time.Sleep(2 * time.Second)
      response := Response{Message: fmt.Sprintf("Processed request with ID %d", req.ID)}
      resCh <- response
    }
  }
}
func main() {
  // 創建根Context
  rootContext := context.Background()
  // 創建用於請求和響應的通道
  reqCh := make(chan Request)
  resCh := make(chan Response)
  // 啓動微服務
  go microservice(rootContext, reqCh, resCh)
  // 創建帶有5秒超時時間的Context
  ctx, cancel := context.WithTimeout(rootContext, 5*time.Second)
  defer cancel()
  // 發送請求到微服務
  for i := 1; i <= 3; i++ {
    req := Request{ID: i}
    reqCh <- req
    select {
    case <-ctx.Done():
      fmt.Println("Request timed out!")
      return
    case res := <-resCh:
      fmt.Println(res.Message)
    }
  }
}

在上面示例中,創建了一個簡單的微服務模擬,它接收來自 reqCh 通道的請求,並將處理結果發送到 resCh 通道。

在主函數中,用帶有 5 秒超時時間的 Context 來確保請求不會無限期等待,同時也能夠處理超時的情況。

7. 最佳實踐與注意事項

7.1 避免在函數庫中使用 Context

通常情況下,應該在函數的參數列表中顯式傳遞 Context,而不是將 Context 放在結構體中。

這樣做可以使函數的行爲更加明確,避免隱藏傳遞的 Context,提高代碼的可讀性和可維護性。

7.2 避免在結構體中嵌入 Context

儘管可以將 Context 作爲結構體的成員嵌入,但這樣的做法通常是不推薦的。

因爲 Context 應該是在函數調用的時候傳遞,而不是嵌入在結構體中。

如果結構體的方法需要使用 Context,應該將 Context 作爲參數傳遞給這些方法。

7.3 注意 Context 的傳遞路徑

在實際應用中,要仔細考慮 Context 的傳遞路徑。

若是在多個函數之間傳遞 Context,確保 Context 的傳遞路徑清晰明瞭,避免出現歧義和混亂。

Context 的傳遞路徑應該儘量短,不要跨越過多的函數調用。

總結

在 Go 語言中,Context 是一個強大的工具,用於處理請求的傳遞、控制和超時等。

通過合理地使用 Context,可以編寫出更加穩定、高效的異步程序,提高系統的健壯性和可維護性。

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