Go语言context上下文传播的工程定位
Go语言context上下文传播是并发编程中请求级生命周期管理的标准方案。每个HTTP请求在进入服务端后,其超时控制、取消信号、请求级元数据都需要沿着函数调用链逐层传递。context包提供了统一接口规范:第一个参数始终是ctx context.Context,所有下游操作接受context并响应取消信号。这个约定贯穿Go标准库和生态,从数据库驱动到RPC框架都遵循context传播规范。
context的核心能力:超时自动取消(WithTimeout/WithDeadline)、手动取消(WithCancel)、请求级KV元数据(WithValue)。三种能力通过函数选项模式组合使用,父context取消时所有子context自动级联取消。
WithTimeout超时控制与级联取消
超时控制是context最常用的场景。HTTP请求设置全局超时,各下游调用在此基础之上设置更短的子超时,任何一级超时都会沿调用链向上传播取消信号。
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
result, err := ProcessOrder(ctx, orderID)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "请求超时", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func ProcessOrder(ctx context.Context, orderID string) (*Order, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
type result struct {
data interface{}
err error
}
userCh := make(chan result, 1)
invCh := make(chan result, 1)
go func() {
data, err := FetchUser(ctx, userID)
userCh <- result{data, err}
}()
go func() {
data, err := FetchInventory(ctx, sku)
invCh <- result{data, err}
}()
select {
case r := <-userCh:
if r.err != nil {
return nil, fmt.Errorf("查询用户失败: %w", r.err)
}
case <-ctx.Done():
return nil, ctx.Err()
}
select {
case r := <-invCh:
if r.err != nil {
return nil, fmt.Errorf("查询库存失败: %w", r.err)
}
case <-ctx.Done():
return nil, ctx.Err()
}
return buildOrder(ctx)
}
级联取消的关键行为:当5秒全局超时触发时,ProcessOrder中的3秒子超时也会被取消,FetchUser和FetchInventory内部的HTTP请求会收到cancel信号并主动断开连接。取消是协作式的——下游函数需要主动检查ctx.Done()或在I/O操作中传入ctx。Go标准库的net/http、database/sql等包已经内置了context支持。
WithCancel手动取消与goroutine泄漏防护
手动取消适用于非超时场景——用户关闭浏览器、客户端主动取消请求、业务逻辑判断不再需要继续执行。goroutine泄漏是手动取消最常见的工程陷阱。
// 错误示例:goroutine泄漏
func LeakExample(parentCtx context.Context) {
ch := make(chan int)
go func() {
result := expensiveComputation()
ch <- result
}()
select {
case res := <-ch:
fmt.Println("结果:", res)
case <-parentCtx.Done():
// parentCtx取消了,但expensiveComputation仍在运行
return
}
}
// 正确写法:传播取消信号
func SafeExample(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
ch := make(chan int, 1)
go func() {
result := cancellableComputation(ctx)
select {
case ch <- result:
case <-ctx.Done():
}
}()
select {
case res := <-ch:
return processResult(res)
case <-ctx.Done():
cancel()
return ctx.Err()
}
}
func cancellableComputation(ctx context.Context) int {
result := 0
for i := 0; i < 1000; i++ {
select {
case <-ctx.Done():
return 0
default:
result += computeStep(i)
}
}
return result
}
goroutine泄漏的检测手段:运行时使用runtime.NumGoroutine()监控goroutine数量变化,持续增长说明存在泄漏。更精细的方案是在测试中用goleak包检测:在TestMain中调用goleak.VerifyTestMain(m),每个测试结束后验证是否有多余的goroutine存活。
WithValue请求级元数据传递
WithValue用于在context中传递请求级别的元数据——TraceID、用户ID、权限信息等。这些数据不需要跨进程传播,只在当前请求的函数调用链中共享。
type contextKey string
const (
keyRequestID contextKey = "request_id"
keyUserID contextKey = "user_id"
keyTraceID contextKey = "trace_id"
)
func WithRequestID(ctx context.Context, reqID string) context.Context {
return context.WithValue(ctx, keyRequestID, reqID)
}
func GetRequestID(ctx context.Context) string {
if v, ok := ctx.Value(keyRequestID).(string); ok {
return v
}
return ""
}
func RequestMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
ctx = WithRequestID(ctx, reqID)
if userID, err := authenticate(r); err == nil {
ctx = context.WithValue(ctx, keyUserID, userID)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func LoggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
logger.Info("request received",
zap.String("request_id", GetRequestID(ctx)),
zap.String("user_id", GetUserID(ctx)),
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
next.ServeHTTP(w, r)
})
}
}
WithValue的使用禁忌:不要用WithValue传递业务数据(如订单对象、配置参数),它只适合传递请求级别的横切关注点元数据。原因是context.WithValue的类型安全需要开发者自行保证,滥用会导致运行时panic。且WithValue的值不会被GC直到context本身被回收,大量数据会增加内存压力。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-shang-xia-wen-chuan-bo-yu-chao-shi-ji/