朕喜技术(ZhenxiJS)
2026.09.07
1 分钟阅读

Go HTTP 服务 goroutine 泄漏:请求超时后后台 goroutine 永久阻塞(channel 发送阻塞)

问题背景

生产环境 Go HTTP API 服务内存持续增长直至 OOM。用户请求 /api/search 接口,每次请求都设置 1 秒 context 超时,后台 goroutine 执行耗时的数据库查询(实际 2 秒),查询完成后将结果发送到无缓冲 channel。handler 因超时 1 秒先返回并退出,后台 goroutine 在 2 秒后尝试发送结果时没有接收方,resultCh <- result 永久阻塞,导致每次请求泄漏恰好 1 个 goroutine。

技术栈:Go 1.22 / net/http / context / goroutine / channel / net/http/pprof / Docker

关键报错/现象:

  • goroutine 数随请求量线性增长且永不回落(从 ~3 涨到 ~33、~103...)
  • 容器内存持续上升直至 OOM,被 docker 重启
  • /debug/pprof/goroutine?debug=1 显示大量 goroutine 阻塞在 main.main.func1.1 /app/main.go:47
  • 该行代码为 resultCh <- result

复现步骤

# 1. 启动环境
cd scenario/2026-09-07-1607
docker compose up -d --build

# 2. 基线观测
curl http://localhost:18080/api/stats
# goroutines: 3

# 3. 发 30 次并发请求
for i in $(seq 1 30); do curl -s http://localhost:18080/api/search & done; wait

# 4. 观测泄漏
curl http://localhost:18080/api/stats
# goroutines: 33  ← 泄漏了 30 个

# 5. pprof 定位
curl "http://localhost:18080/debug/pprof/goroutine?debug=1"

根本原因

三处设计缺陷叠加:

  1. 后台 goroutine 内部函数不感知 context 取消:simulateSlowQuery 固定 time.Sleep(2s) 不检查 ctx.Done(),导致 ctx 取消后它仍跑满 2 秒
  2. 无缓冲 channel 的 send 在无接收方时永久阻塞:handler 超时退出后没人接收
  3. goroutine 内部没有用 select 监听 ctx.Done():盲目尝试 send,无逃生通道

解决办法

修复分三层(防御式编程):

1. 让慢查询感知 context 取消

func simulateSlowQuery(ctx context.Context) string {
    select {
    case <-time.After(2 * time.Second):
        return "db-query-result"
    case <-ctx.Done():
        return "ctx-cancelled"
    }
}

2. resultCh 改为带缓冲 channel

resultCh := make(chan string, 1)

3. goroutine 用 select 同时监听 ctx.Done

go func() {
    result := simulateSlowQuery(ctx)
    select {
    case resultCh <- result:
        // 有接收方或缓冲可用,发送成功
    case <-ctx.Done():
        // ctx 已取消,跳过发送直接退出
    }
}()

验证

bash check.sh

验证结果:

基线 goroutines: 3
60 次请求后 goroutines: 3
goroutine 增长量: 0
✅ PASS: goroutine 泄漏已修复

经验与避坑

  1. 凡启动 goroutine 必须想好退出路径——不是所有 goroutine 都会自然结束
  2. channel send 是阻塞操作——无缓冲 channel 在没有接收方时永久阻塞,Go 不会自动超时
  3. context 是贯穿全链路的——handler 设置了超时,后台 goroutine 必须也监听 ctx.Done()
  4. pprof 定位泄漏:curl /debug/pprof/goroutine?debug=1 会给出所有 goroutine 的阻塞位置堆栈
  5. 推荐组合策略:ctx-aware 查询 + select 监听取消 + 带缓冲 channel,三者叠加才能彻底防泄漏
  6. 调试技巧:用 runtime.NumGoroutine() 和 /api/stats 端点实时观测 goroutine 数

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-07-1611-go-goroutine-leak

分享这篇文章