这是 goroutine 泄漏的铁证;大量 chan receive 或 select 状态且阻塞时间长达数万秒(如 432000s),表明 goroutine 卡死在 channel 操作上无法退出,常见原因包括向 nil channel 操作、无缓冲 channel 发送后无人接收或未关闭。

pprof goroutine 里看到大量 chan receive 是什么信号
这基本就是 goroutine 泄漏的铁证。访问 /debug/pprof/goroutine?debug=2,如果返回中反复出现状态为 chan receive 或 select,且阻塞时间长达数万秒(比如 432000s),说明这些 goroutine 已卡死在 channel 操作上,永远退不出去。
常见原因包括:
- 向
nilchannel 执行或 <code>ch —— 直接永久阻塞 - 无缓冲 channel 写入后,接收方未启动、已退出,或根本没写
close(ch) -
for range ch循环,但 sender 从不关闭 channel,range 就一直等 EOF -
select所有 case 都不可达,又没写default或ctx.Done()分支
为什么 goroutine 数量涨了但 heap profile 看不出明显泄漏
因为 goroutine 泄漏的本质不是堆内存分配失控,而是栈空间和调度元数据持续堆积。每个 goroutine 默认栈约 2KB,卡住后可能因逃逸或闭包捕获而增长到几十 KB,但它不触发大量堆分配,所以 inuse_space 可能平稳甚至下降——GC 还在正常回收堆对象,但卡住的 goroutine 本身及其持有的引用链(比如未关闭的 net.Conn、os.File)让关联资源无法释放。
此时更应关注:
-
NumGoroutine指标是否随请求线性增长 -
/debug/pprof/goroutine?debug=1中相同调用栈数量是否递增 - 系统级指标如
tcp.inuse、file.desc是否同步上涨
http.Server 和 http.Client 超时缺失导致的协程海
这是线上最隐蔽也最高发的泄漏源头。服务端没设超时,客户端也没传 context,一个慢请求就能拖住两个 goroutine(readLoop + writeLoop),空闲连接长期滞留。
必须显式配置:
- 服务端:
http.Server的ReadTimeout、WriteTimeout、IdleTimeout全部设值,不能依赖默认(即 0) - 客户端:不用
http.DefaultClient;所有请求走http.NewRequestWithContext(ctx, ...);http.Transport设置MaxIdleConnsPerHost和IdleConnTimeout - HTTP handler 内启 goroutine 必须监听
req.Context().Done(),不能裸起go func() { ... }()
time.Ticker / time.Timer 不 Stop 的后果
time.NewTicker 和 time.NewTimer 返回的对象底层绑定运行时计时器资源,goroutine 退出后若不显式调用 Stop(),计时器不会自动销毁,会持续占用内存并可能引发 goroutine 泄漏(尤其当 ticker 配合 channel 使用时)。
正确姿势:
-
defer ticker.Stop()必须写在 goroutine 函数体开头附近,不能只靠 context 取消 - 避免用
time.After做长周期等待——它创建的 timer 不可手动 Stop,超时前一直驻留 - 所有
io.Closer类型(resp.Body、*sql.Rows、*redis.Conn)都必须 Close,且 Close 前要确保资源确实拿到了(比如 resp 非 nil)
真正棘手的泄漏往往藏在“看起来没问题”的组合里:比如一个带 context 的 HTTP 请求,内部用了没 context 的第三方库 DB 查询,或者 for range ch 循环里漏掉了 sender 的 close 时机判断。排查时别只盯单点,要顺着引用链一路往下看——谁 hold 住了 channel,谁没关连接,谁忘了 stop timer。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











