因为goroutine堆栈分配在独立内存区域,不计入HeapAlloc统计,heap分析只显示其持有的堆对象而非栈本身。

为什么 goroutine 堆栈本身不直接出现在 heap 分析里
很多人一上来就 go tool pprof http://localhost:6060/debug/pprof/heap,想看“goroutine 占了多少内存”,结果发现堆栈帧(stack frames)几乎不显示——这是因为 Go 的 goroutine 堆栈默认分配在操作系统线程栈或堆外的独立内存区域,不属于 runtime.MemStats.HeapAlloc 统计范畴。真正计入 heap profile 的,是 goroutine 内部显式分配的对象(比如切片、map、结构体指针),而非其执行栈本身。
要定位 goroutine 相关内存问题,得换思路:不是查“堆栈占多少”,而是查“哪些 goroutine 持有大量堆对象”或“哪些 goroutine 长期存活且不断分配”。
-
goroutine接口(/debug/pprof/goroutine?debug=2)只输出当前所有 goroutine 的完整调用栈,不含内存大小,但能快速识别泄漏模式(如大量相同栈未退出) -
heap接口反映的是堆上活跃对象,配合-inuse_space或-alloc_space参数才能看出谁在持续分配 - 长连接程序中,goroutine 泄漏往往表现为:连接未关闭 → goroutine 未 return → 其内部闭包/缓冲区/定时器持续持有堆对象 → 最终 heap 持续增长
如何用 /debug/pprof/goroutine 快速识别泄漏 goroutine
对长连接服务(如 WebSocket、MQTT、gRPC stream),最直接的信号是 /debug/pprof/goroutine?debug=2 返回的 goroutine 数量随时间线性上涨,且大量 goroutine 停留在相似位置(比如阻塞在 conn.Read()、ch <- 或 time.Sleep())。
关键操作:
- 用
wget -O goroutines-1.txt "http://localhost:6060/debug/pprof/goroutine?debug=2"抓两次快照(间隔 30 秒以上) - 对比差异:
diff goroutines-1.txt goroutines-2.txt | grep "^+" | grep -v "runtime.",聚焦新增的非 runtime 栈 - 特别注意带
select { case <-ch:但无对应 close 或超时控制的 goroutine —— 它们大概率已“挂起”且无法被 GC 回收其所引用的变量
heap 分析必须加 -inuse_space 才能看出 goroutine 持有关系
默认 go tool pprof 进入交互模式后执行 top,看到的是累计分配(alloc_space),对泄漏诊断干扰很大。真正有用的是当前活跃占用(inuse_space)——它能暴露哪些函数创建的对象至今没被 GC 回收。
正确命令:
go tool pprof -http=:8081 -inuse_space http://localhost:6060/debug/pprof/heap
然后在 Web 界面点 Top → 切换到 inuse_space 视图,重点关注:
- 函数名含
readLoop、writeLoop、handleConn等长连接典型 handler 的内存占用 - 是否大量
[]byte、strings.Builder、sync.Pool.Get后未归还的对象堆积 - 闭包捕获的 struct 字段(如
conn *net.Conn)间接持有的 buffer、decoder 等
别忽略 runtime.SetMutexProfileFraction 和 block profile
长连接程序常伴随锁竞争和 channel 阻塞,这两者虽不直接吃内存,但会导致 goroutine 长期 suspend,进而让其持有的堆对象无法释放。而默认情况下,/debug/pprof/block 和 /debug/pprof/mutex 是关闭的。
必须在程序启动时显式启用:
import "runtime"
func main() {
runtime.SetBlockProfileRate(1) // 记录所有阻塞事件
runtime.SetMutexProfileFraction(1) // 记录所有互斥锁争用
// ... 启动服务
}
之后才能访问 /debug/pprof/block 和 /debug/pprof/mutex。如果发现某 goroutine 在 sync.(*Mutex).Lock 上阻塞数秒以上,或反复出现在 chan receive 调用栈顶部,基本可判定其生命周期已被意外延长,所持资源无法释放。
真正难排查的不是单个 goroutine,而是它们之间隐式的引用链:一个 goroutine 持有 channel receiver,另一个持有 sender,两者互相等待,GC 无法回收任一端的缓冲区和上下文对象。这种场景下,goroutine + block + heap -inuse_space 三者交叉比对,才是唯一可靠路径。


















