Go链路追踪应使用OpenTelemetry而非runtime/trace:前者通过context透传实现跨服务trace-id串联,支持HTTP/gRPC/DB自动埋点与OTLP导出;后者仅记录单进程内goroutine调度、GC等运行时事件,不参与上下文传播。

Go 里无法用链路追踪(OpenTelemetry)追踪 goroutine 的调度或执行链路——它压根不记录 goroutine ID、不采样函数调用栈、也不关联 runtime 调度事件。你真正需要的不是“追踪 goroutine”,而是分清两个工具的边界:runtime/trace 看调度行为,OpenTelemetry 看业务请求路径。
runtime/trace 能看到什么 goroutine 行为
runtime/trace 是 Go 运行时自带的低层诊断工具,它记录的是 OS 线程(M)、逻辑处理器(P)、goroutine(G)三者之间的绑定与状态切换,比如:
-
G长时间处于Runnable但没被P调度(调度延迟) -
G卡在系统调用(Syscall)或 CGO 调用上 -
G因锁竞争(SyncBlock)或 channel 阻塞而挂起 - GC STW 阶段所有
G被强制暂停的时间点
它不记录函数名、不采样 CPU、不支持自定义标签,也不能导出为 Jaeger/Zipkin 格式。想靠它查 “哪个 handler 启动了最多 goroutine” 或 “goroutine A 是怎么 spawn 出 B 的”,做不到。
OpenTelemetry 不处理 goroutine 关系,只管 context 传递
OpenTelemetry 的 span 是逻辑单元,不是 goroutine 封装。它的父子关系完全依赖 context.Context 显式透传,和 goroutine 数量、生命周期无关。常见误解:
立即学习“go语言免费学习笔记(深入)”;
- 写
go doWork(ctx)就自动继承 span?错——必须确保doWork内部调用tracer.Start(ctx, ...),且ctx是从上游 span 注入过的 - 用
context.WithValue(ctx, key, val)手动传 trace_id?危险——OpenTelemetry 要求用otel.GetTextMapPropagator().Inject()序列化,否则跨服务 header 会失效 - goroutine 内直接用
log.Printf打印 trace_id?不行——log不读 context,得用slog.With("trace_id", ctx.Value(traceKey))或自定义slog.Handler
漏传一次 ctx,下游 span 就变成 root,父子链断裂。这不是 goroutine 问题,是 context 流转问题。
真要分析 goroutine 行为,按场景选工具
别混用,也别强求一个工具解决两类问题:
- 查 “为什么 goroutine 积压不退出”:跑
go tool trace trace.out→ 点击 “Goroutines” 视图,看状态热力图和阻塞原因 - 查 “HTTP 请求触发的 DB 查询为什么慢”:用 OpenTelemetry +
otelsql.InjectDriver,确保db.QueryContext(ctx, ...)中的ctx带 span - 查 “异步任务没日志”:确认启动 goroutine 时传了
ctx,且该ctx来自 span 上下文(如req.Context()),不是context.Background() - 查 “goroutine 泄漏”:用
pprof/goroutine(curl :6060/debug/pprof/goroutine?debug=2)看堆栈,不是runtime/trace
最常被忽略的一点:runtime/trace 必须在 main() 开头就启动,且写入路径不能是 NFS 或 tmpfs——延迟会导致关键调度事件被过滤掉,看起来像“什么都没发生”。


















