

新闻资讯
技术教程goroutine泄漏比数量多更危险,真正拖垮系统的是永不结束的goroutine;应通过pprof监控、避免channel阻塞、配default或超时select、确保channel收发配对、绑定context、使用worker pool复用goroutine。
很多开发者一看到性能下降就下意识减少 goroutine 数量,但真正拖垮系统的是泄漏——那些启动后永远没机会结束的 goroutine。用 pprof 查 /debug/pprof/goroutine?debug=2,如果数量持续上涨,优先排查 channel 阻塞、忘记 close()、或 select 永远走不到 default 分支的

select 时务必配 default 或超时(time.After),避免 goroutine 卡在 channel 接收上context.Context,请求取消后仍可能继续运行面对大量短生命周期任务(如解析日志行、处理 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对纯计算、小数据结构操作(如 json.Marshal 一个 map、strings.ReplaceAll)、或本地内存读写,加 goroutine 只会引入调度和栈分配开销,实测往往更慢。Go 的函数调用本身开销极低,而 goroutine 至少要分配 2KB 栈空间。
fmt.Sprintf、strconv.Atoi、bytes.Equal、map lookup
go tool trace 对比前后,看 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")context.WithCancel,除非你要派生子任务并统一取消ctx 若来自 HTTP handler,它自带 timeout/cancel,直接复用即可context.WithTimeout(context.Background(), 100*time.Millisecond) 强制暴露未响应的 goroutine实际压测中,把 5000 个 goroutine 降为 50 个 worker 后 QPS 提升 3 倍,不是因为“少了”,而是因为泄漏止住了、栈内存稳定了、GC 不再频繁 STW。goroutine 是轻量级的,但不是免费的——它的成本藏在调度器状态、内存页分配、以及你忘记关掉的那一个。