Go语言的context包是并发编程中传递截止时间、取消信号和请求作用域值的标准机制。在微服务架构中,context贯穿HTTP请求处理链、数据库查询、RPC调用全流程,合理的超时控制和取消传播能有效防止goroutine泄漏和资源浪费。本文从context接口设计到实战应用,覆盖超时控制、取消传播、值传递的正确用法和常见陷阱。
context.Context接口设计与核心方法
context.Context是一个接口类型,定义了四个核心方法:
type Context interface {
// Deadline返回上下文的截止时间,ok为false表示未设置
Deadline() (deadline time.Time, ok bool)
// Done返回一个channel,在上下文被取消或超时时关闭
Done() <-chan struct{}
// Err返回取消原因,nil表示未取消
// 取消原因: Canceled(主动取消)或DeadlineExceeded(超时)
Err() error
// Value根据key获取上下文中存储的值
Value(key any) any
}
创建上下文的工厂函数:
package main
import (
"context"
"time"
"fmt"
)
func main() {
// 1. 根上下文(不会被取消,无超时,无值)
rootCtx := context.Background()
// 2. TODO上下文(功能同Background,用于未确定使用哪个上下文的场景)
todoCtx := context.TODO()
// 3. 带超时的上下文
timeoutCtx, cancel := context.WithTimeout(rootCtx, 5*time.Second)
defer cancel() // 必须调用cancel释放资源
// 4. 带截止时间的上下文
deadlineCtx, cancel2 := context.WithDeadline(rootCtx, time.Now().Add(10*time.Second))
defer cancel2()
// 5. 带取消信号的上下文
cancelCtx, cancel3 := context.WithCancel(rootCtx)
defer cancel3()
// 6. 带值的上下文
valueCtx := context.WithValue(rootCtx, "userID", 12345)
// 7. 不带取消的值上下文(Go 1.21+)
noCancelCtx := context.WithoutCancel(rootCtx)
fmt.Println("timeout deadline:", timeoutCtx.Deadline())
fmt.Println("value:", valueCtx.Value("userID"))
}
context超时控制与deadline传播机制
超时控制的核心是WithTimeout创建的上下文在到达截止时间后自动关闭Done() channel,下游操作通过监听该channel实现超时退出:
package main
import (
"context"
"fmt"
"time"
)
// 模拟数据库查询
func queryDB(ctx context.Context, query string) (string, error) {
// 使用ctx.Done()监听取消信号
select {
case <-time.After(3 * time.Second):
return "result: " + query, nil
case <-ctx.Done():
return "", fmt.Errorf("query cancelled: %w", ctx.Err())
}
}
// 模拟RPC调用
func rpcCall(ctx context.Context) (string, error) {
// 派生子上下文,缩短超时时间
rpcCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
select {
case <-time.After(1 * time.Second):
return "rpc result", nil
case <-rpcCtx.Done():
return "", fmt.Errorf("rpc timeout: %w", rpcCtx.Err())
}
}
func handleRequest(ctx context.Context) {
// 从父上下文派生超时上下文
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 并行调用多个下游服务
results := make(chan string, 2)
go func() {
result, err := queryDB(reqCtx, "SELECT * FROM users")
if err != nil {
results <- fmt.Sprintf("DB error: %v", err)
} else {
results <- result
}
}()
go func() {
result, err := rpcCall(reqCtx)
if err != nil {
results <- fmt.Sprintf("RPC error: %v", err)
} else {
results <- result
}
}()
// 等待结果或超时
for i := 0; i < 2; i++ {
select {
case r := <-results:
fmt.Println(r)
case <-reqCtx.Done():
fmt.Println("request timeout:", reqCtx.Err())
return
}
}
}
func main() {
ctx := context.Background()
handleRequest(ctx)
}
deadline传播的关键特性:子上下文的截止时间不能晚于父上下文。如果WithTimeout设置的超时超过了父上下文的剩余时间,子上下文会使用父上下文的截止时间:
func parentChildDeadline() {
// 父上下文5秒超时
parent, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 子上下文尝试设置10秒超时
// 实际截止时间会被限制在父上下文5秒内
child, cancel2 := context.WithTimeout(parent, 10*time.Second)
defer cancel2()
pd, _ := parent.Deadline()
cd, _ := child.Deadline()
fmt.Printf("parent deadline: %v\n", pd)
fmt.Printf("child deadline: %v (clamped to parent)\n", cd)
// 两者截止时间相同
}
context取消信号传播与goroutine泄漏防护
取消信号通过Done() channel向所有子上下文传播。在并发场景中,正确监听Done()是防止goroutine泄漏的关键:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// Worker池:通过context控制worker退出
func workerPool(ctx context.Context, workerCount int, tasks <-chan int) {
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d shutting down: %v\n", workerID, ctx.Err())
return
case task, ok := <-tasks:
if !ok {
fmt.Printf("worker %d: channel closed\n", workerID)
return
}
// 处理任务,使用子上下文控制单个任务超时
taskCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
processTask(taskCtx, task, workerID)
cancel()
}
}
}(i)
}
wg.Wait()
}
func processTask(ctx context.Context, task int, workerID int) {
select {
case <-time.After(1 * time.Second):
fmt.Printf("worker %d completed task %d\n", workerID, task)
case <-ctx.Done():
fmt.Printf("worker %d task %d cancelled: %v\n", workerID, task, ctx.Err())
}
}
// 错误示范:不监听context导致goroutine泄漏
func leakyWorker(ctx context.Context, tasks <-chan int) {
for task := range tasks {
// 没有监听ctx.Done(),即使context被取消也不会退出
time.Sleep(10 * time.Second)
fmt.Println("processed:", task)
}
}
// 正确示范:始终监听context
func safeWorker(ctx context.Context, tasks <-chan int) {
for {
select {
case <-ctx.Done():
return
case task, ok := <-tasks:
if !ok {
return
}
select {
case <-time.After(10 * time.Second):
fmt.Println("processed:", task)
case <-ctx.Done():
return
}
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tasks := make(chan int, 100)
for i := 1; i <= 20; i++ {
tasks <- i
}
close(tasks)
workerPool(ctx, 3, tasks)
}
context在HTTP服务端与客户端的应用
HTTP服务端:每个请求自动携带context,从r.Context()获取:
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func fetchUserFromDB(ctx context.Context, userID int) (*User, error) {
// 模拟数据库查询,监听context
select {
case <-time.After(500 * time.Millisecond):
return &User{ID: userID, Name: "Alice"}, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func userHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求上下文(客户端断开连接时自动取消)
ctx := r.Context()
// 设置响应超时
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
user, err := fetchUserFromDB(ctx, 1)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "request timeout", http.StatusGatewayTimeout)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// 中间件:为每个请求添加超时控制
func timeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/user", userHandler)
handler := timeoutMiddleware(5 * time.Second)(mux)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", handler)
}
HTTP客户端:使用context控制请求超时:
func callExternalAPI(ctx context.Context, url string) ([]byte, error) {
// 从父上下文派生请求超时
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if reqCtx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("api call timeout")
}
return nil, fmt.Errorf("api call failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
context值传递的最佳实践与陷阱
// 正确做法:使用自定义key类型避免冲突
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
authInfoKey contextKey = "authInfo"
)
// 请求中间件注入requestID
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = generateUUID()
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 从context获取requestID
func getRequestID(ctx context.Context) string {
if v, ok := ctx.Value(requestIDKey).(string); ok {
return v
}
return "unknown"
}
// 错误做法:使用string作为key类型
// ctx := context.WithValue(ctx, "userID", 123)
// 任何包都可以使用"userID"字符串,导致冲突
// 正确做法使用自定义类型:
// ctx := context.WithValue(ctx, userIDKey, 123)
WithoutCancel(Go 1.21+)用于在context被取消后仍需要继续执行清理操作的场景:
func cleanupAfterCancel(ctx context.Context) {
// 父context被取消后,使用WithoutCancel派生新上下文
// 新上下文不会被父context的取消信号影响
cleanupCtx := context.WithoutCancel(ctx)
// 仍可设置超时
cleanupCtx, cancel := context.WithTimeout(cleanupCtx, 10*time.Second)
defer cancel()
// 执行清理操作,不受父context取消影响
if err := saveLog(cleanupCtx, logData); err != nil {
log.Printf("cleanup failed: %v", err)
}
}
context性能分析与常见误用排查
// 误用1:在循环中创建带超时的context但忘记cancel
// 会导致context树不断增长,goroutine泄漏
func badPractice() {
ctx := context.Background()
for i := 0; i < 1000; i++ {
// 每次迭代创建新context但不cancel
childCtx, _ := context.WithTimeout(ctx, time.Minute)
// 应该 defer cancel() 或在循环内调用 cancel()
go func(c context.Context, n int) {
select {
case <-time.After(time.Second):
fmt.Println("done", n)
case <-c.Done():
fmt.Println("cancelled", n)
}
}(childCtx, i)
}
// 1000个timer goroutine泄漏
}
// 正确做法
func goodPractice() {
ctx := context.Background()
for i := 0; i < 1000; i++ {
childCtx, cancel := context.WithTimeout(ctx, time.Minute)
go func(c context.Context, n int, cancel context.CancelFunc) {
defer cancel()
select {
case <-time.After(time.Second):
fmt.Println("done", n)
case <-c.Done():
fmt.Println("cancelled", n)
}
}(childCtx, i, cancel)
}
}
// 误用2:使用context传递业务参数
// context应该传递请求作用域的元数据,不是业务逻辑参数
// 错误: ctx = context.WithValue(ctx, "user", userObject)
// 正确: 将userObject作为函数参数传递
// 误用3:在init函数或全局变量中使用context
// context应该随请求生命周期创建和销毁
// 全局context.Background()仅用于根上下文创建
使用errgroup管理多个goroutine的取消传播:
import "golang.org/x/sync/errgroup"
func parallelFetch(ctx context.Context, urls []string) (map[string][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make(map[string][]byte)
var mu sync.Mutex
for _, url := range urls {
url := url // 避免闭包捕获问题
g.Go(func() error {
data, err := callExternalAPI(ctx, url)
if err != nil {
return err // 任意一个失败,ctx自动取消
}
mu.Lock()
results[url] = data
mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // 返回第一个错误
}
return results, nil
}
context在Go并发编程中是不可或缺的基础设施。errgroup结合context可以实现任意一个goroutine出错时自动取消所有并行任务,避免资源浪费。对于需要跨服务传递超时和取消信号的场景,gRPC框架会自动将context的deadline和取消状态编码到HTTP/2头部中传播到下游服务。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-shang-xia-wen-chuan-bo-yu-chao-shi-kong/