登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go goroutine 数量持续增长怎么判断泄漏点

来源:17golang原创

时间:2026-10-14 05:28:06 200浏览 收藏

Go 服务里 goroutine 数量一直往上走,不等于每次增长都是真泄漏。请求高峰、后台任务并发和短时间的等待都会让数量暂时升高;真正值得追的是:同一类任务结束后,数量是否回不到稳定区间。我的排查顺序通常是先记录数量趋势,再看 goroutine profile 的重复堆栈,最后沿着 channel、定时器、网络调用和 context 找没有退出条件的生命周期边界。

判断泄漏点的关键不是盯着某一次 goroutine 数量,而是把“数量持续增长”与“重复堆栈长期存在”对应起来,再确认这些堆栈是否有可达的退出信号。
  • 先看趋势:区分一次性并发峰值和任务完成后的持续累积。
  • 再看堆栈:用 goroutine profile 找出反复出现的等待位置。
  • 最后看生命周期:每个后台 worker 都要能收到取消信号并结束。

官方资料:https://go.dev/doc/diagnostics

先区分增长趋势和瞬时峰值

runtime.NumGoroutine 返回当前 goroutine 数量,适合做趋势观测,但它本身不能告诉你是哪段代码创建了 goroutine。先把采样放进服务指标或日志中,观察同一批请求完成后数量是否回落。下面的示例只负责记录趋势,不把某个固定数字当成泄漏阈值。

package main

import (
    "log"
    "runtime"
    "time"
)

func logGoroutineCount() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop() // 采样器退出时释放定时器资源

    for range ticker.C {
        // 这里只记录当前数量,是否异常要结合业务负载和历史趋势判断。
        log.Printf("goroutines=%d", runtime.NumGoroutine())
    }
}
runtime.NumGoroutine、后台 worker 与数量趋势的静态关系说明图
图1:goroutine 数量趋势与业务 worker 的静态关系说明图,不是运行截图或运行证据。

如果流量停止后数量明显下降,通常更接近正常并发波动;如果负载重复执行、业务任务已经结束,数量却阶梯式抬高,就应该进入堆栈定位。不要只拿“当前值超过多少”做结论,因为不同服务的常驻 goroutine 数量差异很大。

用 goroutine profile 看卡在哪里

Go 官方诊断文档把 goroutine profile 定义为当前 goroutine 的堆栈集合。对 HTTP 服务,可以引入 net/http/pprof 提供诊断端点,再用文本方式查看 profile。生产环境应把诊断端点放在受控网络或鉴权边界内,不要把它直接暴露给公网。

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof" // 注册 /debug/pprof/ 下的标准诊断处理器
)

func serveDiagnostics() {
    // 空白导入已把 pprof 处理器注册到 DefaultServeMux,单独绑定内部地址。
    log.Fatal(http.ListenAndServe("127.0.0.1:6060", http.DefaultServeMux))
}

查看时重点比较两次采样中反复出现的栈顶和等待位置,例如某个 worker 长期停在无发送方的 channel 接收、没有停止条件的定时器循环,或等待永远不会结束的网络调用。profile 是定位线索,不是自动判定器;要回到创建 goroutine 的代码,确认它原本应该由谁结束。

goroutine profile 与 channel、定时器、context.Done 和 worker 退出边界的结构说明图
图2:goroutine profile 与 worker 生命周期边界的结构说明图,不是实际 profile 输出。

沿生命周期检查退出条件

最常见的泄漏点不是 go 关键字本身,而是启动后的工作函数没有一条稳定的退出路径。逐个检查四类等待:channel 是否可能永远收不到值,定时器或 ticker 是否在退出时停止,HTTP 请求是否带有超时,后台循环是否监听 ctx.Done()。

func worker(ctx context.Context, jobs 

如果 profile 中大量 goroutine 都停在同一处,先问“这个等待由谁唤醒”,再问“异常路径有没有关闭或取消”。不要为了让数量下降而粗暴关闭共享 channel;应该明确由创建方负责关闭,由取消方负责广播退出。

用 context 和关闭协议修复

修复时把取消信号从请求边界传到最底层工作函数。请求结束、超时或服务关闭时调用 cancel;后台 worker 用 select 同时监听任务和 ctx.Done()。如果需要等待全部 worker 退出,再用 sync.WaitGroup 让关闭顺序可读,避免主流程已经返回但子 goroutine 仍在后台悬挂。

回归检查可以重复同一组请求,分别记录任务开始、任务完成和取消后的 goroutine profile。理想结果不是数量立刻回到启动时的数字,而是在常驻 goroutine 加上当前业务并发后进入稳定区间;如果每轮任务完成后仍然新增一批相同堆栈,说明还有一条生命周期没有闭合。

常见问题

goroutine 数量高就一定是泄漏吗?

不一定。高并发、长轮询、批处理和常驻 worker 都可能让数量较高。要看负载结束后的回落趋势,以及重复堆栈是否长期存在。

应该先看 heap profile 还是 goroutine profile?

标题问题是 goroutine 数量持续增长时,先看 goroutine profile 更直接;如果同时出现内存增长,再结合 heap profile 判断 goroutine 是否只是被对象引用住,不能把两类 profile 混为一个结论。

为什么给 channel 加缓冲仍然没有解决泄漏?

缓冲只能改变发送方暂时等待的位置,不能替代关闭协议和取消信号。如果消费者没有退出条件,缓冲区耗尽后仍会长期阻塞。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>