goroutine泄漏即启动后永久阻塞不退出,铁证是runtime.numgoroutine()持续上涨且pprof debug=2显示大量goroutine卡在chan receive/select/semacquire/io wait;需监控请求后不回落、压测后不恢复、长期单调上升三种趋势,结合三处日志、pprof快照对比、goleak测试拦截定位修复。

goroutine 启动后不退出,请求堆积怎么办
Go 服务在高并发下出现响应变慢、内存持续上涨,往往不是因为并发量大,而是大量 goroutine 卡在阻塞操作里没释放。比如调用未设超时的 http.Client.Do、读取未关闭的 io.Reader、向满的无缓冲 channel 发送数据——这些都会让 goroutine 永久休眠。
必须为每个可能阻塞的操作绑定生命周期控制:
- 所有网络调用统一用
context.WithTimeout或context.WithDeadline包裹,超时后自动取消 - 避免直接用
http.DefaultClient;改用自定义http.Client并设置Timeout、Transport的IdleConnTimeout - 向
channel发送前,优先用select+default分支做非阻塞尝试,或确保 channel 有足够缓冲 - 启动
goroutine的地方,务必检查是否传入了可取消的context.Context,并在函数入口处监听ctx.Done()
微服务间调用该用 goroutine 还是 sync.WaitGroup
不是“用不用 goroutine”,而是“在哪一层调度”。同步 RPC(如 gRPC)本身已是异步 I/O 封装,客户端调用 client.SomeMethod(ctx, req) 内部已用 goroutine 处理底层连接和序列化——你再套一层 go 只会增加调度开销,还容易漏掉错误处理和资源回收。
真正需要显式并发的场景是:多个独立服务调用之间无依赖,且需并行获取结果。这时才用 sync.WaitGroup 或 errgroup.Group:
- 用
errgroup.Group更安全:它自动传播第一个错误、支持上下文取消、无需手动Done() - 避免把不同服务的调用塞进同一个
WaitGroup而不区分超时——订单服务和用户服务响应时间差异大,应分别设context.WithTimeout - 不要在 HTTP handler 里直接启一堆
goroutine去调下游,而应封装成可复用、带熔断/重试/限流的 client 方法
goroutine 泄漏怎么快速定位
线上服务 runtime.NumGoroutine() 持续增长,但 PProf 的 /debug/pprof/goroutine?debug=2 显示大量状态为 semacquire 或 chan receive 的协程,基本就是泄漏了。
关键检查点:
- 所有
for range ch循环,确认ch是否会在某处被close();没 close 的 channel 会导致接收方永久阻塞 - 所有
select块,是否遗漏default或case ,导致协程卡死在等待 channel - HTTP handler 中启动的
goroutine,是否把http.Request.Context()传递进去,而不是用context.Background() - 数据库查询是否用了
rows.Close()?没关的*sql.Rows会拖住连接池,间接导致后续请求新建更多goroutine等待连接
高并发下 goroutine 数量该设多少
GOMAXPROCS 控制的是 P(逻辑处理器)数量,不是 goroutine 上限。Go 运行时能轻松调度百万级 goroutine,瓶颈从来不在“数量”,而在“它们干的事”。
真正要约束的是并发执行的 I/O 密集型任务数:
- 数据库连接池大小(如
db.SetMaxOpenConns(n))应作为硬上限参考,goroutine 并发数不宜显著超过它 - HTTP 客户端连接池(
http.Transport.MaxIdleConns)决定了你能同时发起多少连接,超出部分会排队等待 - 对下游服务的并发调用,应配合令牌桶(
rate.Limiter)或 worker pool 模式限流,避免打垮对方 - 别迷信“每个请求一个 goroutine”——对简单 CRUD 接口,直接同步处理反而更高效;只在明确存在 I/O 等待且能并行时才拆分











