Posted in

【Go应用链路追踪失效真相】:context.WithValue被滥用导致traceID丢失的4种隐蔽路径

第一章:Go应用链路追踪失效的典型现象与影响

当Go应用集成OpenTelemetry或Jaeger等链路追踪方案后,常出现“看似正常上报、实则链路断裂”的隐性失效,这类问题往往在生产环境压测或故障排查时才暴露,危害远超预期。

追踪数据缺失的直观表现

  • HTTP请求进入服务后无Span生成,或仅生成独立根Span(缺少parent-id传递);
  • 跨goroutine调用(如go func() { ... }())中Span上下文丢失,子任务完全脱离追踪树;
  • 异步操作(如消息队列消费、定时任务)上报的Span显示为孤立节点,无法关联上游触发链路。

根本原因与高频陷阱

Go语言的并发模型天然要求显式传播上下文。若未使用context.WithValue(ctx, key, value)配合otel.GetTextMapPropagator().Inject()完成跨goroutine/跨协议的SpanContext透传,追踪即告中断。常见错误包括:

  • 直接将context.Background()用于异步协程启动;
  • 使用http.DefaultClient发起请求而未注入trace header;
  • database/sql驱动中未启用otelsql插件,导致DB调用无Span。

快速验证方法

执行以下诊断命令,检查基础传播是否生效:

# 启动带trace header的curl测试(模拟上游调用)
curl -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
     http://localhost:8080/api/v1/users

观察服务日志或Jaeger UI:若返回Span的trace_idtraceparent中不一致,或parent_id为空,则证明HTTP header解析或context注入失败。此时需检查中间件是否调用otelhttp.NewHandler()包装路由,以及handler内是否通过r.Context()获取并延续上下文。

失效场景 检查要点 修复动作
HTTP客户端调用丢失trace http.Client.Transport是否为otelhttp.NewTransport() 替换默认transport,启用自动header注入
goroutine内无Span 是否用trace.ContextWithSpan(ctx, span)派生新ctx? 禁止直接传原始ctx,必须显式携带Span
Gin框架无Span 中间件是否注册otelgin.Middleware()且顺序正确? 确保在路由匹配前执行,避免跳过中间件

第二章:context.WithValue滥用导致traceID丢失的核心机制

2.1 context.Value的底层实现与内存模型解析

context.Value 并非独立存储结构,而是依托 context.Context 接口的具体实现(如 valueCtx)以链表形式嵌套承载:

type valueCtx struct {
    Context
    key, val interface{}
}
  • key 通常为导出类型或 uintptr,用于唯一标识键;
  • val 为任意值,其内存布局由 Go 的逃逸分析决定,可能分配在堆上;
  • 每次 WithValue 调用均创建新 valueCtx 实例,形成不可变链表。

数据同步机制

无锁设计:所有字段均为只读,依赖不可变性规避竞态;值读取需遍历链表,时间复杂度 O(n)。

内存布局特征

字段 类型 是否逃逸 说明
key interface{} 接口头含类型+数据指针
val interface{} 视值大小 小对象可能栈分配
graph TD
    A[ctx.Background] --> B[valueCtx key1=val1]
    B --> C[valueCtx key2=val2]
    C --> D[valueCtx key3=val3]

2.2 traceID注入时机与context传递链的断点实测分析

注入时机关键节点

traceID 必须在请求进入网关(如 Spring Cloud Gateway)的首个 Filter 中生成并写入 ServerWebExchange,早于任何业务线程切换。延迟注入将导致下游 MDCThreadLocal 上下文为空。

断点实测现象

ReactorContext 传递链中插入断点发现:

  • WebFilter 阶段:traceID 存在于 reactor.util.context.ContextView
  • @Async 方法内:Context 未自动传播,traceID 丢失

核心修复代码

// 使用 ContextWriter 显式传播
Mono.just("data")
    .contextWrite(ctx -> ctx.put("traceID", MDC.get("traceID"))) // 注入当前traceID
    .flatMap(val -> Mono.fromCallable(() -> {
        String tid = ReactorContextUtils.getTraceID(); // 自定义工具类
        return "processed-by-" + tid;
    }).subscribeOn(Schedulers.boundedElastic()));

逻辑说明contextWritetraceID 写入 Reactor 的 ContextsubscribeOn 切换线程后,需通过 ReactorContextUtilsContextView 安全提取——否则 MDC.get() 在新线程为空。

传播失败场景对比

场景 是否继承 traceID 原因
Mono.transform() Context 自动继承
@Async + Future Spring AOP 未增强 Reactor 上下文
graph TD
    A[Gateway WebFilter] -->|生成并写入Context| B[RouterFunction]
    B --> C[HandlerMethod]
    C --> D[Reactor Mono/Flux]
    D -->|contextWrite| E[boundedElastic线程池]
    E --> F[traceID 可见]
    D -->|无contextWrite| G[common-pool线程] 
    G --> H[traceID 丢失]

2.3 Goroutine泄漏场景下context生命周期错配的复现与验证

复现泄漏的核心模式

当子goroutine持有了父context但未随父context取消而退出,即发生生命周期错配:

func leakyHandler(ctx context.Context) {
    go func() {
        select {
        case <-time.After(5 * time.Second): // 忽略ctx.Done()
            fmt.Println("work done")
        }
    }()
}

逻辑分析:该goroutine未监听ctx.Done(),即使父请求已超时或取消,协程仍运行5秒后才退出,造成资源滞留。time.After返回独立timer,不响应context取消信号。

关键参数说明

  • ctx: 传入的request-scoped context(如http.Request.Context()
  • time.After(5s): 创建不可取消的延迟通道,是泄漏根源

常见错配类型对比

场景 是否监听ctx.Done() 是否泄漏 原因
select { case <-ctx.Done(): ... } 及时响应取消
time.Sleep(5s) 完全阻塞,无视context
time.After(5s) 返回独立timer,无context绑定

验证流程(mermaid)

graph TD
    A[启动goroutine] --> B{是否监听ctx.Done?}
    B -->|否| C[持续运行至硬超时]
    B -->|是| D[受context控制退出]
    C --> E[Goroutine泄漏确认]

2.4 中间件/HTTP Handler中隐式context覆盖的典型案例剖析

问题场景还原

当多个中间件连续调用 r = r.WithContext(newCtx) 但复用同一 *http.Request 变量时,后置中间件会覆盖前置中间件注入的 context 值。

典型错误代码

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), "user_id", "123")
        r = r.WithContext(ctx) // ✅ 正确赋值给 r
        next.ServeHTTP(w, r)
    })
}

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), "req_id", "abc")
        r = r.WithContext(ctx) // ✅ 看似正确,但若 AuthMiddleware 已修改 r,则此处 ctx 基于新 r.Context()
        log.Printf("req_id: %s", ctx.Value("req_id"))
        next.ServeHTTP(w, r)
    })
}

逻辑分析r.WithContext() 返回新 request 实例,但若中间件未严格链式传递(如 next.ServeHTTP(w, r.WithContext(...))),而使用局部变量 r 覆盖,会导致 context 被后续中间件反复覆盖——"user_id" 在 LoggingMiddleware 执行后仍存在,但若顺序颠倒或并发请求混用,值可能丢失或错位。

隐式覆盖风险对比

场景 Context 值是否可预测 原因
严格链式 r.WithContext().WithContext() ✅ 是 每次基于前序 context 构建
多中间件独立 r = r.WithContext(x) ❌ 否 后执行者覆盖前执行者的 context 键值
graph TD
    A[原始Request] --> B[AuthMW: r.WithContext user_id]
    B --> C[LoggingMW: r.WithContext req_id]
    C --> D[Handler: r.Context() 包含两者]
    B -.-> E[若LoggingMW误读原始r.Context()] --> F[丢失user_id]

2.5 defer+recover捕获异常时context意外重置的调试实践

现象复现

当在 http.HandlerFunc 中使用 defer recover() 捕获 panic 后,后续 r.Context() 返回的 context.Context 突然丢失 requestID、超时等关键值。

根本原因

Go HTTP server 在 panic 后会新建一个 clean contextcontext.Background())用于错误处理,而非复用原请求上下文。

func handler(w http.ResponseWriter, r *http.Request) {
    // 原上下文含 requestID、timeout 等
    log.Printf("before: %v", r.Context().Value("requestID")) // "req-123"

    defer func() {
        if err := recover(); err != nil {
            // 此处 r.Context() 已被 server 内部重置!
            log.Printf("after panic: %v", r.Context().Value("requestID")) // <nil>
        }
    }()

    panic("unexpected error")
}

逻辑分析http.ServeHTTPrecover() 后调用 server.go 中的 logf(),该路径未传递原始 r.Context(),而是构造新 context。r 结构体本身未变,但其 ctx 字段已被 runtime 替换。

解决方案对比

方案 是否保留 Context 风险 适用场景
提前拷贝 ctx := r.Context() 到闭包 推荐,零侵入
使用 http.Handler 包装器统一拦截 中(需全局注册) 中大型服务
依赖 r.Context() 原生恢复 高(不可靠) 不推荐

调试技巧

  • defer 中立即保存 r.Context() 引用;
  • 使用 pprof + runtime/debug.Stack() 定位 panic 栈中 context 替换点;
  • 启用 GODEBUG=http2debug=2 观察 context 生命周期。

第三章:四种隐蔽路径的深度溯源与验证方法

3.1 异步任务(go func)中未显式传递context导致traceID归零

go func() 启动协程时,若直接使用外层 context.Context 变量(而非传参),协程将捕获闭包中已失效或未携带 traceID 的 context 副本。

典型错误写法

func handleRequest(ctx context.Context, userID string) {
    // ctx 包含有效 traceID(如 via middleware 注入)
    go func() {
        log.Info("processing", "trace_id", getTraceID(ctx)) // ❌ ctx 可能被父函数返回后回收
        db.Query(ctx, "SELECT ...") // ctx 已无 span,traceID=0
    }()
}

逻辑分析ctx 在 goroutine 中以闭包形式引用,但父函数返回后其关联的 span 被结束,ctx.Value(traceKey) 返回空字符串或默认零值;getTraceID() 内部若 fallback 为 ,即导致链路断裂。

正确做法对比

方式 是否保留 traceID 风险点
go func(ctx context.Context) 显式传参
闭包引用外层 ctx context 生命周期不匹配

修复方案

go func(ctx context.Context) {
    log.Info("processing", "trace_id", getTraceID(ctx))
    db.Query(ctx, "SELECT ...")
}(ctx) // ✅ 立即传入当前有效 ctx

3.2 第三方库(如sqlx、redis-go)自动封装context引发的trace断裂

sqlxredis-go 等库内部新建 context.WithTimeoutcontext.Background(),原始 trace context(含 traceID/spanID)即被丢弃:

// ❌ 错误示例:sqlx.QueryRowContext 隐式截断 trace
row := db.QueryRowContext(context.Background(), "SELECT name FROM users WHERE id=$1", 123)

此处 context.Background() 无 span 信息,OpenTelemetry SDK 无法延续父 span,导致链路断裂。

常见断裂点对比

默认行为 是否继承传入 context
sqlx 直接使用传入 context ✅(但需显式传递)
redis-go WithContext(ctx) 必须显式调用 ❌ 若漏调则 fallback 到 context.Background()

修复模式

  • 始终显式透传 context(禁止 Background()/TODO()
  • 使用 otelwrap 等中间件自动注入 span
graph TD
    A[HTTP Handler] --> B[context.WithValue<span>]
    B --> C[sqlx.QueryRowContext]
    C --> D[DB Driver]
    D --> E[Span Continuation]

3.3 grpc metadata与http.Header双向转换时traceID覆盖的边界条件

traceID注入的优先级链路

gRPC Metadata 与 HTTP Header 双向转换时,traceID 的归属权由注入时机与键名规范共同决定:

  • grpc-trace-bin(binary)优先级高于 trace-id(text)
  • 多个同名 key 存在时,后写入者覆盖先写入者

关键边界场景

  • 客户端同时设置 metadata.Set("trace-id", "t1")metadata.Set("grpc-trace-bin", binTrace)
  • HTTP 网关将 trace-id: t2 显式写入 Header 后再调用 metadata.FromOutgoingContext()
  • gRPC Server 端 metadata.MD 解析时,grpc-trace-bin 自动解码为 w3c.TraceParent 并覆盖文本 trace-id
// 示例:HTTP Header → gRPC Metadata 转换中 traceID 覆盖逻辑
md := metadata.MD{}
md.Set("trace-id", "t1")                    // 文本 traceID
md.Set("grpc-trace-bin", hex.EncodeToString([]byte("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"))) // W3C TraceParent binary
// → 最终生效 traceID 为 binary 解码后的 "0af7651916cd43dd8448eb211c80319c"

逻辑分析grpc-go 内部 transport/http_util.goMetadataFromHeaders 会优先解析 grpc-trace-bin,并调用 otel.GetTextMapPropagator().Extract(),其默认实现(如 W3CTraceContext) 会提取 trace-id 字段并覆盖已有文本值。参数 md 是可变引用,覆盖不可逆。

场景 trace-id 是否被覆盖 触发条件
仅设 trace-id 无 binary header
先设 trace-id 后设 grpc-trace-bin binary 解析成功且含 trace-id
grpc-trace-bin 解析失败 base64/格式错误,降级保留文本
graph TD
    A[HTTP Header] -->|含 grpc-trace-bin| B[Parse Binary]
    A -->|仅 trace-id| C[Use as-is]
    B -->|Success| D[Extract W3C trace-id]
    D --> E[Overwrite existing trace-id in MD]
    B -->|Fail| C

第四章:生产级修复方案与工程化防御体系构建

4.1 基于context.WithValue的替代方案:自定义Context接口与Wrapper模式

context.WithValue 易导致类型不安全、键冲突与调试困难。更健壮的路径是显式封装——定义领域专属 Context 接口,并通过 Wrapper 模式注入依赖。

自定义 Context 接口设计

type RequestContext interface {
    context.Context
    UserID() string
    TenantID() string
    TraceID() string
}

该接口继承 context.Context,强制实现关键业务字段访问器,避免 interface{} 类型断言风险;所有方法均为只读,保障不可变性。

Wrapper 实现示例

type requestCtx struct {
    context.Context
    userID, tenantID, traceID string
}

func (r *requestCtx) UserID() string     { return r.userID }
func (r *requestCtx) TenantID() string   { return r.tenantID }
func (r *requestCtx) TraceID() string    { return r.traceID }

func WithRequestContext(parent context.Context, opts ...RequestOption) RequestContext {
    c := &requestCtx{Context: parent}
    for _, opt := range opts {
        opt(c)
    }
    return c
}

requestCtx 是私有结构体,确保外部无法绕过构造函数直接实例化;RequestOption 函数式选项支持可扩展字段注入。

方案 类型安全 键冲突风险 调试友好性 静态检查支持
context.WithValue
Wrapper 模式
graph TD
    A[原始 context.Context] --> B[WithRequestContext]
    B --> C[requestCtx 实例]
    C --> D[UserID/TenantID/TraceID 方法调用]
    D --> E[编译期类型校验]

4.2 静态代码扫描(golangci-lint插件)识别高危context操作的落地实践

在微服务场景中,context.WithTimeout/WithCancel 被误用为包级变量或跨 goroutine 复用,极易引发 goroutine 泄漏与上下文提前取消。

配置 golangci-lint 检测规则

启用 exportloopref 和自定义 govet context 检查项:

linters-settings:
  govet:
    check-shadowing: true
  gocritic:
    disabled-checks:
      - "context-keys-type"

典型误用模式识别

以下代码触发 context-should-not-be-global 规则:

var globalCtx = context.WithTimeout(context.Background(), 5*time.Second) // ❌ 错误:全局复用 context

func handler(w http.ResponseWriter, r *http.Request) {
    select {
    case <-globalCtx.Done(): // 可能被其他请求提前 cancel
        http.Error(w, "timeout", http.StatusServiceUnavailable)
    }
}

分析globalCtx 是不可变 context 实例,但其内部 cancel 函数未被调用,且 Done() 通道永不关闭,导致 select 永久阻塞。golangci-lint 通过控制流图(CFG)识别 context 构造脱离 request 生命周期。

检测能力对比表

工具 检测 context.WithCancel 泄漏 识别跨函数 context 传递异常 支持自定义 context key 类型检查
golangci-lint + govulncheck
graph TD
    A[源码解析] --> B[AST 中提取 context.With* 调用]
    B --> C{是否出现在函数体外?}
    C -->|是| D[标记高危]
    C -->|否| E[追踪 cancel 调用路径]
    E --> F[未匹配 defer cancel → 告警]

4.3 TraceID透传一致性校验中间件的设计与压测验证

核心设计目标

确保跨服务调用链中 TraceID 在 HTTP/GRPC/RPC 协议间零丢失、零篡改,支持 Spring Cloud、Dubbo、gRPC 多生态。

数据同步机制

采用「双写校验 + 异步快照」策略:请求入口注入 TraceID 到 MDC,出口前比对 X-B3-TraceIdtrace_id 字段一致性。

public class TraceConsistencyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        String headerId = ((HttpServletRequest) req).getHeader("X-B3-TraceId");
        String mdcId = MDC.get("trace_id");
        if (!Objects.equals(headerId, mdcId)) {
            Metrics.counter("trace.mismatch").increment(); // 上报不一致指标
        }
        chain.doFilter(req, res);
    }
}

逻辑分析:在 Filter 链路中捕获原始 Header 与 MDC 中的 TraceID,触发不一致计数器;参数 trace.mismatch 用于 Prometheus 实时告警。

压测关键结果

并发线程 P99 延迟(ms) 不一致率 CPU 使用率
500 2.1 0.0002% 38%
2000 3.7 0.0005% 62%

链路校验流程

graph TD
    A[HTTP入口] --> B{Header含TraceID?}
    B -->|是| C[写入MDC]
    B -->|否| D[生成新TraceID]
    C & D --> E[业务逻辑执行]
    E --> F[响应前比对Header/MDC]
    F --> G[记录差异并上报]

4.4 eBPF辅助诊断:在内核层观测Go runtime中context传播路径的可行性探索

Go 的 context.Context 本质是用户态的 goroutine 局部变量传递机制,不直接暴露至内核。但其调度生命周期(如 runtime.gopark/runtime.goready)会触发系统调用与上下文切换事件。

核心可观测点

  • sched_trace(需 CONFIG_SCHED_DEBUG=y
  • tracepoint:sched:sched_switch
  • uprobe on runtime.propagateCtx(Go 1.22+ 符号导出)

eBPF 探针示例(BCC Python)

# attach uprobe to context propagation helper (if symbol available)
b.attach_uprobe(name="/usr/lib/go/bin/go", sym="runtime.propagateCtx",
                fn_name="trace_propagate", pid=-1)

此探针捕获 propagateCtx 调用时的栈帧与寄存器状态;需 Go 二进制启用 -gcflags="all=-l" 禁用内联以保留符号,并配合 bpftool prog dump jited 验证指令合法性。

关键限制对比

维度 用户态追踪(pprof/trace) eBPF 内核层观测
上下文关联性 依赖 GODEBUG=asyncpreemptoff=1 防抢占丢失 可绑定 pid+tgid+cpu 元组,但无法还原 context.Value 键值对
时效性 毫秒级采样延迟 微秒级事件捕获(需 ringbuf 零拷贝传输)
graph TD
    A[goroutine 执行] --> B{调用 context.WithCancel}
    B --> C[生成新 context 实例]
    C --> D[runtime.propagateCtx uprobe 触发]
    D --> E[eBPF map 记录 goroutine ID + 时间戳 + 父 context hash]
    E --> F[用户态聚合重建传播链]

第五章:从链路追踪失效反思Go生态的上下文治理范式

一次生产事故的还原:OpenTelemetry Span丢失始末

某电商核心订单服务在v2.3.1版本上线后,Jaeger UI中约37%的HTTP请求完全缺失Span,且trace_id字段为空。日志中未报错,otelhttp中间件注册正常,context.WithValue(ctx, key, val)被高频调用——但span.FromContext(ctx)在下游goroutine中始终返回nil。抓包确认Traceparent头已随HTTP请求发出,问题锁定在Go运行时上下文传递环节。

goroutine泄漏上下文的典型模式

以下代码片段在真实项目中反复出现:

func handleOrder(ctx context.Context, orderID string) {
    span := trace.SpanFromContext(ctx)
    go func() { // ⚠️ 危险:goroutine脱离原始ctx生命周期
        // 此处span为nil!因为闭包捕获的是ctx变量名,而非其值
        log.Printf("Processing %s", orderID)
        // 后续调用span.AddEvent()静默失败
    }()
}

根本原因在于:Go的context.Context不可继承的值类型引用go语句启动的新goroutine无法自动继承父goroutine的context生命周期,必须显式传参。

Go标准库上下文传播的隐式断裂点

场景 是否自动传播context 备注
http.HandlerFunc调用链 net/http内部调用handler.ServeHTTP()时保留ctx
time.AfterFunc()回调 回调函数接收不到原始ctx,需手动包装
sql.DB.QueryRowContext() context.Context作为参数显式传递
sync.Pool.Get()返回对象 对象本身不携带ctx,需业务层注入

基于context.WithCancelCause的防御性实践

Go 1.22引入的WithContextCancelCause可显式标记中断原因,避免因context.Canceled掩盖真实错误:

ctx, cancel := context.WithCancelCause(parentCtx)
defer cancel(errors.New("order processing timeout"))
// 后续所有WithXXX操作均继承该因果链
childCtx := context.WithValue(ctx, orderKey, orderID)
// 即使goroutine异常退出,cancelCause仍可追溯根因

上下文键设计的反模式与正解

错误实践:使用stringint作为context key

ctx = context.WithValue(ctx, "user_id", 123) // ❌ 冲突风险高,无类型安全

正确实践:定义私有未导出类型确保唯一性

type userIDKey struct{}
func WithUserID(ctx context.Context, id int64) context.Context {
    return context.WithValue(ctx, userIDKey{}, id)
}
func UserIDFromContext(ctx context.Context) (int64, bool) {
    v, ok := ctx.Value(userIDKey{}).(int64)
    return v, ok
}

Mermaid流程图:Span生命周期断裂诊断路径

flowchart TD
    A[HTTP请求进入] --> B[otelhttp.Handler注入Span]
    B --> C{Span是否成功写入ctx?}
    C -->|是| D[调用service.ProcessOrder]
    C -->|否| E[检查middleware顺序/中间件panic]
    D --> F[goroutine启动异步任务]
    F --> G{是否显式传递ctx?}
    G -->|否| H[Span丢失 - 进入监控告警]
    G -->|是| I[Span正常上报]

生产环境上下文健康度巡检脚本

在Kubernetes集群中部署以下cronjob,每5分钟检测Pod内goroutine的context存活率:

# 检查当前进程所有goroutine中含span的占比
gdb -p $(pgrep myapp) -ex 'set $n=0' -ex 'set $span=0' \
    -ex 'thread apply all p (void*)runtime·getg()->m->curg->context' \
    -ex 'quit' 2>/dev/null | \
    grep -c "span\|Span" # 实际需解析输出结构化数据

上下文治理的SLO量化指标

  • Context Propagation Success Rate ≥ 99.95%(基于eBPF拦截context.WithValue调用并校验下游SpanFromContext非nil)
  • Goroutine Context Leak Rate
  • Context Key Collision Incidents = 0(CI阶段强制扫描context.WithValue中key类型是否为私有结构体)

依赖库上下文兼容性清单维护

团队建立内部CONTEXT_COMPATIBILITY.md,记录关键依赖对context的支持状态: 库名 版本 Context支持方式 已验证场景
github.com/redis/go-redis/v9 v9.0.5 WithContext()方法 GetContext, SetContext
github.com/jackc/pgx/v5 v5.4.0 所有Query方法带Context参数 QueryRow, Exec
github.com/aws/aws-sdk-go-v2/config v1.18.24 LoadDefaultConfig(ctx) 初始化阶段必须传ctx

重构案例:将遗留goroutine池升级为context-aware worker

原代码使用全局sync.Pool导致context丢失:

var workerPool = sync.Pool{New: func() interface{} { return &worker{} }}
func dispatchAsync(task Task) {
    w := workerPool.Get().(*worker)
    go w.process(task) // ❌ ctx未传递
}

重构后强制context绑定:

type ContextWorker struct {
    ctx context.Context
    fn  func(context.Context, Task)
}
func (w *ContextWorker) Process(ctx context.Context, task Task) {
    w.ctx = ctx // 显式绑定
    w.fn(ctx, task)
}

关注异构系统集成,打通服务之间的最后一公里。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注