根本问题是*grpc.clientconn实例反复创建未关闭或长期持有,导致http/2连接及goroutine泄露;http客户端需close resp.body以防连接池耗尽;sql操作须及时close rows/tx;goroutine须监听context.done()、关闭channel或waitgroup协调。

gRPC ClientConn 泄露:复用错、Close 忘、defer 放错位置
根本问题不是“连接开太多”,而是*grpc.ClientConn实例被反复创建却没关,或被存到全局/单例里长期持有。每个*grpc.ClientConn底层维持一个 HTTP/2 连接(可承载成百上千 stream),但若你不调conn.Close(),它就永远不释放 goroutine 和缓冲区。
常见错误现象:pprof 里看到大量 http2.(*ClientConn).readLoop 或 transport.loopyWriter,数量随请求量线性上涨;lsof -i 显示 ESTABLISHED 连接持续累积。
- 所有
grpc.Dial()调用点必须配对defer conn.Close(),且要放在err != nil检查之后、任何 RPC 调用之前 - 禁止在 handler 内写
conn, _ := grpc.Dial(...); defer conn.Close()—— 每次请求都新建再关,TCP 握手开销大,Close()又是异步清理,高频操作反而触发 transport 层 goroutine 泄露 - 若复用
*grpc.ClientConn(推荐做法),必须确保它生命周期可控:比如注入到 service struct 中,由服务启动时创建、关闭时调conn.Close();切勿存在 map/sync.Pool 里却不做健康检查或过期淘汰 -
grpc.Dial的WithBlock和WithTimeout与连接生命周期无关,只影响 dial 阶段;误用会导致初始化卡死或返回nil conn后 panic
HTTP client.Body 泄露:不 Close 就不归还连接池
Go 的 http.Client 默认 keep-alive,但复用前提是:你必须完整读取并关闭 resp.Body。否则连接卡在 persistConn.readLoop,既不释放也不复用,最终耗尽文件描述符。
典型报错:socket: too many open files;lsof -i :443 | wc -l 持续上涨;pprof/goroutine 里堆栈停在 net.(*pollDesc).Wait。
-
defer resp.Body.Close()必须写在err == nil分支内,且紧接在client.Do()或client.Get()之后 —— 如果resp是nil,调Close()会 panic - 非 2xx 响应(如 403、500)的
resp.Body也必须读完再关,不能跳过;可用io.Copy(io.Discard, resp.Body)快速消费 - 别直接用
http.DefaultClient,它的Transport可能被第三方库偷偷改掉;应显式构造&http.Client{Transport: &http.Transport{...}} - 关键 Transport 参数:设
MaxIdleConns和MaxIdleConnsPerHost至少 500;IdleConnTimeout推荐 30s(太长占 fd,太短频建连)
database/sql 连接泄露:InUse 卡高位、Rows/Tx 没 Close
不是连接“没关”,而是 *sql.Rows 或 *sql.Tx 拿到连接后没释放。看 db.Stats().InUse —— 如果 QPS 20 时它长期稳定在 150+,基本就是泄漏了。
pprof 里搜 database/sql.(*DB).conn 或 (*Rows).Next,如果大量 goroutine 卡在这,说明上一个连接没归还。
-
rows, err := db.Query()后,即使err != nil,rows也可能非nil并持有连接;必须判rows != nil后再defer rows.Close() - 循环里调
db.QueryRow(),defer rows.Close()一定要写在循环内部,否则只关最后一次 - 事务分支多时,
tx.Commit()和tx.Rollback()必须成对出现;建议用defer func() { if r := recover(); r != nil { tx.Rollback() } }()+ 显式 error 分支 rollback - 别在函数入口写
defer db.Close()——db是连接池句柄,关了整个池就废了;真正该关的是rows、tx、stmt
goroutine 泄露:context.Done() 没监听、channel 没关、WaitGroup 漏 done
泄漏的 goroutine 不会自己退出,除非它主动检查 ctx.Done()、收到关闭的 channel、或被 sync.WaitGroup 正确等待。没有这些机制,它就卡在 I/O 或 select 上,内存和 goroutine 数缓慢爬升。
pprof 里看到一堆 goroutine 停在 select、chan receive 或 net.Conn.Read,大概率是没响应 cancel 信号。
- 所有带网络 I/O 的 goroutine,开头就要
select { case ,并在后续 I/O 调用中传入该 ctx - 启动 goroutine 前调
wg.Add(1),退出前必须wg.Done();defer wg.Done()要写在 goroutine 函数最顶部,避免 panic 时漏调 - 用 channel 做任务分发时,发送方完成所有数据后必须
close(ch),接收方用for range ch自动退出;别用for { select { case x := 死循环 - 避免在 goroutine 里启动子 goroutine 却不传 ctx 或不等 wg —— 比如
go func() { result := ,如果 <code>rpcCall永不返回,这个 goroutine 就永远挂住
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











