Golang中Context包使用場景和示例詳解

本文結(jié)合示例代碼講解一下context包的幾種使用場景。
控制子協(xié)程退出
context包提供了一種機(jī)制,可以在多個(gè)goroutine之間進(jìn)行通信和控制。使用Context包能夠有效地控制程序的并發(fā)性,提高程序的健壯性和性能。
Golang是沒有辦法讓其他goroutine退出的,goroutine只能自己退出。之所以說context包可以控制子協(xié)程退出意思是子協(xié)程可以接收到主協(xié)程發(fā)出的退出信號(hào),然后自己退出??慈缦率纠a:
package main
import (
	"context"
	"errors"
	"sync"
)
func request(ctx context.Context, url string) error {
	result := make(chan int)
	err := make(chan error)
	go func() {
		// 假如isSuccess是請(qǐng)求返回的結(jié)果,成功則通過result傳遞成功信息,錯(cuò)誤通過error傳遞錯(cuò)誤信息
		isSuccess := true
		if isSuccess {
			result <- 1
		} else {
			err <- errors.New("some error happen")
		}
	}()
	select {
	case <-ctx.Done():
		// 其他請(qǐng)求失敗
		return ctx.Err()
	case e := <-err:
		// 本次請(qǐng)求失敗,返回錯(cuò)誤信息
		return e
	case <-result:
		// 本此請(qǐng)求成功,不返回錯(cuò)誤信息
		return nil
	}
}
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	// 調(diào)用接口a
	err := request(ctx, "https://xxx.com/a")
	if err != nil {
		return
	}
	wg := sync.WaitGroup{}
	// 調(diào)用接口b
	wg.Add(1)
	go func() {
		defer wg.Done()
		err := request(ctx, "https://xxx.com/b")
		if err != nil {
			cancel()
		}
	}()
	// 調(diào)用接口c
	wg.Add(1)
	go func() {
		defer wg.Done()
		err := request(ctx, "https://xxx.com/c")
		if err != nil {
			cancel()
		}
	}()
	wg.Wait()
}首先調(diào)用context.WithCancel方法構(gòu)造了一個(gè)Context和返回了一個(gè)cancel函數(shù),其他goroutine調(diào)用的方法都傳入了這個(gè)Context作為第一個(gè)參數(shù),當(dāng)主goroutine想要告訴所有g(shù)oroutine需要退出的時(shí)候,通過調(diào)用cancel函數(shù)把退出的信息告訴所有的goroutine。所有g(shù)oroutine通過監(jiān)聽ctx.Done返回的channel得到退出信號(hào)然后退出。
超時(shí)控制
例如查詢數(shù)據(jù)庫、調(diào)用RPC服務(wù)、調(diào)用HTTP接口等場景,這些操作都是阻塞的,如果一直不返回?cái)?shù)據(jù)的話,會(huì)影響產(chǎn)品的用戶體驗(yàn)。針對(duì)這些情況的解決方式通常是設(shè)置一個(gè)超時(shí)間,超過后自動(dòng)取消操作。
使用context包中的WithDeadline和WithTimeout方法可以實(shí)現(xiàn)這個(gè)解決方法。先看如下例子:
package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // 輸出 "context deadline exceeded"
    }
}因?yàn)樵O(shè)置的超時(shí)時(shí)間是50毫秒,所以select會(huì)進(jìn)入第二個(gè)case,會(huì)輸出“context deadline exceeded”。
Golang的net/http包發(fā)起http請(qǐng)求的時(shí)候是實(shí)現(xiàn)了超時(shí)控制的,看如下代碼:
package main
import (
	"context"
	"io"
	"log"
	"net/http"
	"time"
)
func main() {
	req, err := http.NewRequest(http.MethodGet, "https://www.baidu.com", nil)
	if err != nil {
		log.Fatal(err)
	}
	// 構(gòu)造一個(gè)超時(shí)間為50毫秒的Context
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	req = req.WithContext(ctx)
	
	c := &http.Client{}
	res, err := c.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	out, err := io.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(string(out))
}執(zhí)行后會(huì)輸出“context deadline exceeded”,如果將context.WithTimeout方法的timeout參數(shù)調(diào)大一些,就可以看到正常的返回?cái)?shù)據(jù)。
上下文傳遞數(shù)據(jù)
這個(gè)作用在鏈路追蹤中非常重要,鏈路追蹤需要將traceID層層往下傳遞,在服務(wù)間傳遞。
type traceIdKey struct{}{}
// 定義固定的Key
var TraceIdKey = traceIdKey{}
func ServeHTTP(w http.ResponseWriter, req *http.Request){
  // 首先從請(qǐng)求中獲取到traceID
  traceId := getTraceIdFromRequest(req)
  // 將Key存入Context中
  ctx := context.WithValue(req.Context(), TraceIdKey, traceId)
  // 設(shè)置超時(shí)時(shí)間
  ctx = context.WithTimeout(ctx, time.Second)
  // 攜帶traceId發(fā)起rpc請(qǐng)求
  repResp := RequestRPC(ctx, ...)
  // 攜帶traceId查詢DB
  dbResp := RequestDB(ctx, ...)
  // ...
}
func RequestRPC(ctx context.Context, ...) interface{} {
    // 獲取traceid,在調(diào)用rpc時(shí)記錄日志
    traceId, _ := ctx.Value(TraceIdKey)
    // 發(fā)起請(qǐng)求
    // ...
    return
}接收到請(qǐng)求后,通過req獲取到traceId并記錄到Context中,在調(diào)用其他RPC服務(wù)和查詢DB時(shí),傳入構(gòu)造的Context。在后續(xù)代碼中,可以通過Context拿到存入的traceId。















 
 
 














 
 
 
 