goroutine 泄露本质是无人收尾、唤醒或关闭channel,表现为pprof中chan receive长期阻塞、堆栈停在runtime.chanrecv或selectgo,导致内存和文件句柄持续增长。

Go 语言里 Goroutine 泄露不是“学不会”,而是“没意识到它卡在哪”。真正的问题从来不是 goroutine 开多了,而是开出去之后——没人收尾、没人唤醒、没人关 channel。
pprof 看到 chan receive 卡死几小时,基本就是泄露了
访问 /debug/pprof/goroutine?debug=2,如果大量堆栈停留在:
-
runtime.gopark+runtime.chanrecv或runtime.selectgo - 阻塞时间显示为
432000s(120 小时)、2592000s(30 天)这类超长值 - 调用栈指向
net/http.readLoop、golang.org/x/crypto/ssh或你自己的for range ch
说明这些 goroutine 已经失去响应能力。它们不 panic、不报错,只是静静吃内存和文件句柄——直到 NumGoroutine 涨到数万,tcp.inuse 居高不下,服务开始变慢或 OOM。
无缓冲 channel 的发送/接收失配是最常见根因
不是语法错,是逻辑断点。一个 ch := make(chan int) 后只写不读、只读不写,或写完没关,都会让 goroutine 永久挂起。
ch 在无接收方时会永久阻塞(哪怕只差一行 <code>go func(){ )在发送方退出且未 <code>close(ch)时,接收方永远等不到 EOF-
for range ch遇到未关闭的 channel,循环永不结束 - 多个 goroutine 往同一个
chan写,但没人明确负责close—— ownership 模糊导致谁也不敢关
这类问题在压测或低流量时段不易暴露,上线后随请求累积缓慢恶化。
HTTP client/server 超时缺失会批量制造泄漏
每个未超时的 HTTP 连接背后,至少挂着 2 个 goroutine(readLoop + writeLoop)。它们不释放,就一直占着 fd 和栈内存。
- 客户端用
http.DefaultClient:没有连接池限制、没有超时、keepalive 连接永不关闭 - 服务端没设
ReadTimeout/WriteTimeout/IdleTimeout:空闲连接长期滞留,goroutine 不退出 -
http.NewRequest未绑定context.WithTimeout:下游响应慢时,上游 goroutine 无限等待 - 忘记
resp.Body.Close():底层连接无法复用,触发新建连接 + 新 goroutine
修复必须两端同步:客户端用带 timeout 的 http.Client + 显式 Close();服务端配置全链路超时,并启用 http.TimeoutHandler 做兜底。
Context 传递断裂让 goroutine 失去“叫停”能力
goroutine 启动时没接收 ctx,或接收了但没在阻塞点监听 ctx.Done(),等于给协程发了单程票。
- handler 里
go func() { /* 调用第三方 API */ }()没传req.Context() - select 中漏掉
case 分支,导致 cancel 信号被忽略 - 子 goroutine 创建了新 channel,但没把父 ctx 透传进去,形成 context 断层
- 用
context.Background()替代req.Context(),失去请求生命周期绑定
真正难排查的,往往是那种“看起来有 ctx,但 select 里没监听”的情况——它不崩溃,只是悄悄多活几分钟、几小时,最后积少成多。
最易被忽略的点:泄露往往不是单点错误,而是组合拳。比如一个 for range ch 接收方,配上未 close 的生产者,再叠加上没超时的 HTTP 请求,三者嵌套,pprof 看到的就只剩一长串 chan receive,根本看不出原始入口在哪。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











