context包的核心设计理念
Go语言的context包是并发编程中上下文传播的标准方案。每一个HTTP请求、gRPC调用、数据库查询都应该接收一个context参数,由调用方控制超时、取消和值传递。context的设计遵循一个原则:生命周期由创建者控制,消费者只能读取或派生子context。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
正确使用context的第一条规则:不要存储context,而是将其作为函数第一个参数逐层传递。
超时控制的传播链路
超时控制是context最常用的场景。在一个典型的微服务调用链中,网关层设置全局超时,每个下游服务从父context派生自己的子超时:
func HandleAPI(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
user, err := fetchUser(ctx, userID)
if err != nil {
handleError(w, err)
return
}
orders, err := fetchOrders(ctx, user.ID)
}
func fetchUser(ctx context.Context, id string) (*User, error) {
childCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(childCtx, "GET", userURL, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if childCtx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("用户服务超时: %w", childCtx.Err())
}
return nil, err
}
defer resp.Body.Close()
return user, nil
}
关键点:子context的超时不能超过父context。当父context在2秒后超时,即使子context设置了3秒超时也会在2秒时被取消。这种传播机制确保上游超时能可靠地级联到所有下游调用。
取消信号的传播与资源释放
context的取消信号通过Done通道传播。当父context被取消时,所有从它派生的子context也会被取消。
func longRunningTask(ctx context.Context) error {
resultCh := make(chan string, 1)
errCh := make(chan error, 1)
go func() {
result, err := doExpensiveWork()
if err != nil {
errCh <- err
return
}
resultCh <- result
}()
select {
case <-ctx.Done():
cleanupPartialWork()
return ctx.Err()
case result := <-resultCh:
processResult(result)
return nil
case err := <-errCh:
return err
}
}
取消操作最常忽略的陷阱是资源泄漏。当context被取消后,正在执行的HTTP请求、数据库查询等操作不会自动中止。必须检查ctx.Done()并主动释放资源。数据库驱动如pgx和go-sql-driver都支持context取消中断查询。
WithValue的请求范围值传递
context.WithValue用于在请求范围内传递值,典型场景是链路追踪ID、用户身份信息等。
type contextKey string
const (
traceIDKey contextKey = "trace-id"
userIDKey contextKey = "user-id"
)
func GetTraceID(ctx context.Context) string {
if v, ok := ctx.Value(traceIDKey).(string); ok {
return v
}
return ""
}
func TraceMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID := generateTraceID()
ctx := context.WithValue(r.Context(), traceIDKey, traceID)
w.Header().Set("X-Trace-ID", traceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
WithValue的正确使用边界:仅传递请求范围内的元数据,不传递业务逻辑参数。如果一个函数需要某个值才能工作应该将其声明为函数参数。
context泄漏的排查与预防
最常见的context泄漏场景是:在循环中创建context但不及时取消。每次调用WithTimeout/WithCancel都会在内部启动一个timer goroutine。
// 错误示例:cancel被丢弃
func badLoop() {
for i := 0; i < 1000; i++ {
ctx, _ := context.WithTimeout(context.Background(), 10*time.Minute)
go process(ctx, i) // 1000个timer goroutine泄漏
}
}
// 正确写法:及时调用cancel
func goodLoop() {
for i := 0; i < 1000; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
go func(idx int) {
defer cancel()
process(ctx, idx)
}(i)
}
}
排查context泄漏的工具:使用runtime.NumGoroutine()监控goroutine数量,配合pprof的goroutine profile查看阻塞在timer的goroutine。生产环境建议在所有WithTimeout/WithCancel调用后紧跟defer cancel(),这是Go并发编程中的基本纪律。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-shang-xia-wen-chuan-bo-yu-chao-shi-kong/