先看 /debug/pprof/goroutine?debug=2 查完整调用栈,定位卡在 chan receive、select 漏 default 或 netpoll(如无超时 http.Get);若堆栈反复出现同一行业务代码(如 client.go:72),即为泄漏点。

goroutine 数暴涨但没 panic,先看 /debug/pprof/goroutine?debug=2
这不是“程序出错了”,而是 runtime 已经在 silently 拖着几千个 goroutine 跑了几个小时。默认访问 /debug/pprof/goroutine 只返回摘要(?debug=1),根本看不到谁卡在哪。?debug=2 才输出完整调用栈,必须加。
常见卡点有三类:chan receive、select(尤其漏写 default)、netpoll(比如 http.Get 无超时);如果堆栈里反复出现同一行业务代码(如 client.go:72),基本就是它没退出。
- 没开 HTTP server?用
runtime/pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)手动 dump,第二个参数必须是2 - 别只盯着数字——
runtime.NumGoroutine()启动后记一次,压测 5 分钟再查,从 5 到 3000+ 且不回落,就是泄漏信号 - 生产环境暴露
/debug/pprof/前务必加 IP 白名单或 Basic Auth,否则指标全泄露
for 循环里直接 go process(item) 是最危险的幻觉
你以为并发提速,实际是批量制造僵尸 goroutine:每个 goroutine 至少占 2KB 栈空间,10000 个就是 20MB;更糟的是闭包捕获变量地址,所有 goroutine 共享同一个 item,结果全处理错数据。
错误写法:for _, item := range items { go process(item) } → item 地址被复用
立即学习“go语言免费学习笔记(深入)”;
正确写法:for _, item := range items { go func(x Item) { process(x) }(item) },显式传参切断引用链
- 更稳的做法是弃裸启,改用 worker pool:固定 8–32 个长期 goroutine + 缓冲 channel
- 缓冲大小不能拍脑袋:按
峰值 QPS × 平均耗时 × 2算,比如 200 QPS × 0.1s = 20,再 ×2 安全系数 →make(chan Job, 40) - 千万别用无缓冲 channel 当任务队列,上游 handler 会直接阻塞,等于把背压甩给 HTTP server
pprof/block 显示 chan send 占比超 40%,说明生产者远多于消费者
这和 CPU 高无关,是典型的任务堆积——goroutine 在等 channel 接收,但接收端要么没启、要么卡住、要么数量太少。此时 runtime.gopark 在 pprof 里占比会飙升,P 大量时间花在挂起/唤醒,而非干活。
- 检查 worker 是否监听
ctx.Done():每个 worker 必须有select { case job := - I/O 密集型(HTTP/DB)worker 数设为
2×–4× CPU 核数;CPU 密集型(加解密)严格 ≈runtime.GOMAXPROCS() - 若发现大量 goroutine 卡在
http.Transport.RoundTrip或db.Query,立刻查resp.Body.Close()是否漏关——连接池会悄悄起新 goroutine 管理闲置连接,越积越多
heap profile 看不出泄漏,但 goroutine 数持续涨,大概率是“钉住”了对象
goroutine 本身内存小,但它只要活着,就可能持有着大对象指针:比如闭包捕获了含 []byte 的 struct,或 channel 接收者一直挂着 map 引用。GC 不敢回收这些对象,heap 里却看不到源头。
验证方法:/debug/pprof/goroutine?debug=2 和 /debug/pprof/heap?gc=1 必须一起看——前者找“活着但不该活”的协程,后者确认 inuse_space 是否持续上涨(不是 allocs)。
- 两次 heap 采样间隔至少 30 秒,且都带
?gc=1强制 GC 后采集,净增长才可疑 - 对比命令用
go tool pprof -base heap0.pb.gz heap1.pb.gz,它会标出新增存活对象来源 - 日志打太勤、
time.AfterFunc传闭包没清引用、select 漏 default,都是隐形泄漏元凶,不报错但几小时后就崩
?debug=2 堆栈里那一长串函数调用中,哪一行是你该删的、哪一行是你该加 ctx.Done() 的——多数泄漏藏在“看起来没问题”的异步初始化或后台 ticker 里。



















