Go 语言中 runtime.Stack 追踪 Goroutine 堆栈
runtime.Stack默认仅捕获当前goroutine堆栈,4KB缓冲区易导致截断。需设置大缓冲区并将第二个参数设为true才能获取所有goroutine堆栈。死锁诊断中可安全使用,但高并发频繁调用会导致停顿。无法直接获取指定goroutine堆栈,需借助内部调用或调试工具。
先说几个核心判断:runtime.Stack 这个函数,很多 Go 开发者都以为它跟 panic 一样,能瞬间把完整的调用栈甩到脸上。但实际用起来,要么返回空,要么只给个半截的堆栈信息,搞得人一头雾水。
问题出在几个容易被忽略的细节上。默认情况下,runtime.Stack 只捕获当前 goroutine 的堆栈,而且缓冲区大小只有 4KB。对于稍微深一点的嵌套调用,4KB 根本不够用,直接截断。如果你在主 goroutine 或者一个刚启动的轻量级 goroutine 里调用它,看到的大概就是几行 runtime 初始化代码,甚至直接返回 0 字节。

为什么 runtime.Stack 经常返回空或截断的堆栈?
说到底,这是两个参数没配好造成的。第一个参数 buf 必须足够大,比如 make([]byte, 64*1024)。如果传进去的缓冲区大小,返回的实际写入长度又小于 len(buf),那就意味着堆栈信息被截断了。第二个参数是关键中的关键——必须设为 true,才能拿到所有 goroutine 的堆栈;设为 false 的话,只抓当前 goroutine。另外,别在 defer 里无条件调用它。一旦发生 panic,部分 goroutine 可能已经退出了,堆栈信息也就不可靠了。
如何安全获取所有 goroutine 的完整堆栈用于诊断死锁?
死锁场景下,runtime.Gosched() 确实派不上用场,但 runtime.Stack 还能正常工作。关键在于避开对运行时调度器状态的依赖。最稳妥的做法是提前预留一个大缓冲区,在信号处理函数或者独立的 goroutine 中触发采集。
看一段非阻塞采集的示例代码:
buf := make([]byte, 2*1024*1024) // 2MB,足够覆盖数千个 goroutine
n := runtime.Stack(buf, true)
if n == len(buf) {
// 虽然截断的概率极低,但可以加个警告或者退回到更小粒度的采样
}
log.Printf("dumped %d bytes of goroutine stacks", n)
需要警惕的是,runtime.Stack 是同步操作。如果在高并发的关键路径上频繁调用,尤其是 all=true 时需要遍历全部 goroutine 结构体,会导致明显的停顿。生产环境建议只在受控入口使用,比如通过 SIGQUIT 信号触发,或者借助 pprof 的 /debug/pprof/goroutine?debug=2 接口。
与 pprof 的 goroutine profile 对比:该选哪个?
runtime.Stack 返回的是即时、带源码行号的文本堆栈,输出格式跟 panic 的调用链很像。而 pprof.Lookup("goroutine").WriteTo 输出的是采样摘要,默认只包含正在运行或阻塞中的 goroutine,而且没有文件名和行号。
选择依据其实很明确:
- 想查“此刻谁卡在 select/channel 上”?那就用
runtime.Stack(buf, true),看看完整的阻塞点在哪。 - 想查“是否有 goroutine 泄漏,长期存在但不该存在”?那就用
pprof.Lookup("goroutine").WriteTo,然后对比多次快照的差异。 - 想集成到 HTTP 接口里,方便随时查看堆栈?直接复用
/debug/pprof/goroutine?debug=2就行。它底层调用的就是runtime.Stack,而且已经帮你处理好了缓冲区和字符编码的问题。
跨 goroutine 获取指定 goroutine 的堆栈可行吗?
做不到。runtime.Stack 没有接收 goroutine ID 或者指针的参数,Go 运行时也根本不导出 goroutine 句柄。想获取“指定 goroutine”的堆栈,只能走一些间接路线:
- 在目标 goroutine 内部主动调用
runtime.Stack,然后把结果通过 channel 发出去或者写日志。 - 使用
debug.SetTraceback("system")提升 panic 时的堆栈深度,然后手动触发一次 panic —— 当然,仅限调试环境。 - 借助第三方工具,比如 Delve 调试器的
goroutines -t命令,在进程外部 attach 后按 ID 查看堆栈。
千万别想着用 unsafe 包或者反射去读 runtime.g 结构体。Go 运行时的内部字段布局从来都不保证稳定,更何况 Go 1.22 已经重构了 goroutine 状态机,这种 hack 行为随时可能崩溃。


































