goroutine泄漏比数量多更危险,真正拖垮系统的是永不结束的goroutine;应通过pprof监控、避免channel阻塞、配default或超时select、确保channel收发配对、绑定context、使用worker pool复用goroutine。

goroutine 泄漏比数量多更危险
很多开发者一看到性能下降就下意识减少 goroutine 数量,但真正拖垮系统的是泄漏——那些启动后永远没机会结束的 goroutine。用 pprof 查 /debug/pprof/goroutine?debug=2,如果数量持续上涨,优先排查 channel 阻塞、忘记 close()、或 select 永远走不到 default 分支的情况。
- 用
select时务必配default或超时(time.After),避免 goroutine 卡在 channel 接收上 - 向无缓冲 channel 发送前,确保有另一端在接收;否则该 goroutine 会永久阻塞
- HTTP handler 中启的 goroutine,若未绑定
context.Context,请求取消后仍可能继续运行
用 worker pool 替代每任务一个 goroutine
面对大量短生命周期任务(如解析日志行、处理 HTTP 请求体),直接为每个任务起一个 goroutine 会导致调度开销激增、内存碎片化、GC 压力变大。worker pool 能复用 goroutine,控制并发上限,也便于统一 cancel 和监控。
var wg sync.WaitGroup
jobs := make(chan *Task, 100)
for i := 0; i < runtime.NumCPU(); i++ {
go func() {
defer wg.Done()
for job := range jobs {
job.Process()
}
}()
wg.Add(1)
}
// 投递任务
for _, t := range tasks {
jobs <- t
}
close(jobs)
wg.Wait()
- 池大小通常设为
runtime.NumCPU()或略高(2–4 倍),而非硬编码 100/1000 - channel 缓冲区不宜过大,否则会掩盖背压问题,建议设为池大小的 2–3 倍
- 避免在 worker 内部再起 goroutine,除非明确需要异步回调且已做生命周期管理
同步操作别强行 goroutine 化
对纯计算、小数据结构操作(如 json.Marshal 一个 map、strings.ReplaceAll)、或本地内存读写,加 goroutine 只会引入调度和栈分配开销,实测往往更慢。Go 的函数调用本身开销极低,而 goroutine 至少要分配 2KB 栈空间。
- 以下情况几乎从不值得起 goroutine:
fmt.Sprintf、strconv.Atoi、bytes.Equal、map lookup - IO 密集型才适合并发:HTTP 请求、DB 查询、文件读写——但也要注意连接池限制,不是越多越快
- 用
go tool trace对比前后,看 goroutine 创建/阻塞时间占比,比拍脑袋优化更可靠
context.WithCancel 是 goroutine 生命周期的开关
只要 goroutine 涉及 IO 或等待,就必须接收 context.Context 并在 select 中监听 ctx.Done()。没有它,就等于放弃对 goroutine 的主动控制权,尤其在微服务中,超时、重试、熔断都依赖这个信号。
go func(ctx context.Context, url string) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return // 正常退出
}
log.Printf("req failed: %v", err)
return
}
defer resp.Body.Close()
// ...
}(ctx, "https://api.example.com")- 不要在 goroutine 内部再调
context.WithCancel,除非你要派生子任务并统一取消 - 传入的
ctx若来自 HTTP handler,它自带 timeout/cancel,直接复用即可 - 测试时用
context.WithTimeout(context.Background(), 100*time.Millisecond)强制暴露未响应的 goroutine
实际压测中,把 5000 个 goroutine 降为 50 个 worker 后 QPS 提升 3 倍,不是因为“少了”,而是因为泄漏止住了、栈内存稳定了、GC 不再频繁 STW。goroutine 是轻量级的,但不是免费的——它的成本藏在调度器状态、内存页分配、以及你忘记关掉的那一个。











