goroutine泄漏的本质是goroutine被阻塞在同步原语上无法调度,却持续占用栈空间和资源;常见于无消费者channel写入、无生产者channel读取、context未取消、WaitGroup计数不匹配四类阻塞模式。

Go 微服务的网络并发能力不是靠“调优”出来的,而是由 net/http 默认行为 + goroutine 调度机制天然决定的——每个 HTTP 请求进来,http.Server 就自动启一个 goroutine 去处理,无需你手动写 go。但这也正是最容易出问题的地方:看似省事,实则埋雷。
为什么每个请求都自动跑在 goroutine 里?
net/http 的 Server.Serve 内部对每个新连接调用 srv.ServeConn,而每个连接上的请求(尤其是 keep-alive 场景)会通过 serverHandler.ServeHTTP 派发到 handler;关键点在于:这个派发过程本身不阻塞主线程,且每个请求的 HandlerFunc 执行都在独立 goroutine 中启动。
这不是文档里写的“建议”,而是标准库的硬编码逻辑(见 src/net/http/server.go 中 conn.serve 方法)。你写 http.HandleFunc("/", handler),就等于把 handler 注册进了一个自动并发调度器。
- 好处:开箱即用,上万连接不需额外线程池或事件循环封装
- 风险:如果
handler里有阻塞操作(如未设超时的http.Client.Do、死循环、未加锁的全局 map 写入),整个 goroutine 就卡住,资源不释放 - 注意:
http.Server本身不管理 goroutine 生命周期,它只负责“启动”,不负责“回收”
goroutine 泄露的典型场景和自查方式
微服务跑几天后内存持续上涨、runtime.NumGoroutine() 从几百涨到几万,大概率是 goroutine 泄露。最常见原因不是忘了 go,而是忘了“怎么让它结束”。
立即学习“go语言免费学习笔记(深入)”;
-
select没配default或timeout:比如select { case 在 <code>ch永不关闭时,goroutine 永远挂起 - channel 发送未被接收:向无缓冲 channel 发送数据,但没人接收,发送 goroutine 永久阻塞
- 忘记调用
cancel():用context.WithTimeout创建子 context 后,没在 handler 返回前显式调用cancel,底层 timer 和 goroutine 不会被清理 - HTTP 长轮询或 streaming response 中,连接断开时没监听
req.Context().Done():客户端关连接,但你的 goroutine 还在等下一条消息
快速验证:在服务中加个 debug endpoint,返回 runtime.NumGoroutine() 和 debug.ReadGCStats,观察增长趋势是否与请求量线性相关。
什么时候该用 goroutine 池,而不是无节制 spawn?
不是所有场景都适合“来一个请求启一个 goroutine”。当你的 handler 里要发起下游 IO(比如调第三方 API、查 DB),且这些 IO 具备明显瓶颈(如下游限流为 100 QPS),放任每个请求都启 goroutine,只会把压力转嫁给下游,并可能触发雪崩。
- 适用 goroutine 池的信号:
pprof显示大量 goroutine 处于syscall或IO wait状态,且数量远超 CPU 核心数 - 不要自己手写池:用成熟库如
golang.org/x/sync/semaphore控制并发数,或github.com/marusama/semaphore封装更友好 - 关键参数不是“最大并发数”,而是“每个下游服务的独立信号量”:比如对支付服务限 20 并发,对用户服务限 50,并行调用时分别 acquire,避免互相挤占
- 注意:池化只管“执行入口”,不解决 handler 内部泄露;仍需配合
context超时和 defer cancel
channel 用错比不用还危险
很多人以为用了 channel 就安全了,结果写出一堆隐蔽死锁。微服务里 channel 主要用在两类地方:跨 goroutine 传递请求上下文数据(如 trace ID)、构建 worker pipeline。但错误用法极多。
- 用无缓冲 channel 做“同步等待”:比如
done := make(chan struct{}),然后go func() { ...; close(done) }(),但主 goroutine 忘记—— 这不是泄露,是逻辑遗漏,但效果类似 - 把 channel 当共享状态容器:例如把
map[string]chan int当成服务间通信总线,但没做sync.RWMutex保护 map 本身(channel 可以并发 send/receive,但 map 不行) - 在 HTTP handler 里创建带缓冲 channel 却不消费:比如
ch := make(chan int, 100),往里塞 100 条日志后就不管了,channel 对象本身一直持有内存,直到 goroutine 结束 - 真正安全的模式:channel 只用于 goroutine 之间的一次性数据传递,生命周期明确(sender 和 receiver 都知情),且至少一方有超时或取消机制
复杂点永远不在语法,而在控制流的可见性——你能一眼看出某个 goroutine 会在哪一行结束吗?如果不能,它大概率已经失控了。


















