用/debug/pprof/goroutine?debug=2查看阻塞的goroutine:它输出所有goroutine完整栈,重点识别状态为chan receive、select、semacquire或runtime.gopark的协程,结合堆栈中重复出现的业务代码(如client.go:72)或同步原语(如sync.(*WaitGroup).Wait)定位卡点。

怎么用 pprof 查看阻塞的 goroutine
Go 程序卡住时,最直接的办法是看哪些 goroutine 停在了系统调用、锁或 channel 操作上。net/http/pprof 提供的 /debug/pprof/goroutine?debug=2 是首选入口——它会输出所有 goroutine 的完整栈,包括正在等待什么。
启用方式很简单:在程序里加几行(不需要改业务逻辑):
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
然后访问 curl http://localhost:6060/debug/pprof/goroutine?debug=2。重点看状态为 syscall、chan receive、semacquire 或反复出现的 runtime.gopark 的 goroutine。
常见陷阱:
立即学习“go语言免费学习笔记(深入)”;
- 没开 pprof 服务就直接 curl,返回 404 —— 确保 import 和 goroutine 启动都执行了
-
?debug=1只显示摘要(goroutine 数量),看不出卡在哪,必须用?debug=2 - 生产环境暴露 pprof 接口有风险,建议只监听本地地址或加简单认证
为什么 runtime.Stack 不适合定位卡死问题
runtime.Stack 能打印当前所有 goroutine 栈,但默认只捕获正在运行的 M 上的 goroutine,对已 park 的(比如等 channel、锁)可能漏掉,而且输出无结构、难过滤。
更关键的是:它不区分 goroutine 状态,没法一眼看出哪个是“真卡住”,哪个只是短暂休眠。相比之下,pprof/goroutine?debug=2 明确标注了每个 goroutine 的等待原因(如 chan send (nil chan) 就是往 nil channel 发送,必卡)。
如果你非要用 runtime.Stack,至少得这样写才接近 pprof 效果:
buf := make([]byte, 2<<20)
n := runtime.Stack(buf, true) // 第二个参数为 true 表示 all goroutines
fmt.Printf("%s", buf[:n])
但依然不推荐——信息冗余、无分类、不方便 grep。
如何快速识别常见的卡死模式
从 pprof/goroutine?debug=2 输出里,盯住这几类栈帧组合:
-
select+runtime.gopark+ 多个chan receive:说明 select 分支全阻塞,没 default,且 channel 都没人收/发 -
sync.(*Mutex).Lock+runtime.semacquire1:大概率死锁,两个 goroutine 互相持有一把锁又想拿另一把 -
io.ReadFull/net.Conn.Read+runtime.gopark:底层 socket 没数据也没关闭,连接挂死了 -
runtime.chansend或runtime.chanrecv单独出现:channel 是无缓冲且对方不配合,或缓冲满/空后没人操作
注意:单看一个 goroutine 栈不够,要交叉比对——比如看到 A 在等 channel,就立刻搜谁在往这个 channel 写;看到锁等待,就找谁持有同名 sync.Mutex 实例。
调试时要不要加 GODEBUG=schedtrace=1000
这个环境变量会让 Go 运行时每秒打印调度器统计(goroutine 创建/销毁、M/P 状态等),对长期卡死有用,但对“瞬间卡住又恢复”的场景反而干扰大——日志刷屏、难定位具体 goroutine。
更适合的组合是:
- 先用
pprof/goroutine?debug=2快速抓现场 - 如果卡得不稳,加
GODEBUG=gctrace=1看 GC 是否频繁停顿(GC 也会让 goroutine 看似卡住) - 怀疑调度器问题(比如大量 goroutine 但 CPU 很低),再开
schedtrace,并配合scheddetail=1
真正卡死的问题,90% 以上靠 pprof/goroutine?debug=2 就能定位到具体那几行代码和 channel/mutex 实例——别一上来就堆调试开关。


















