goroutine泄漏典型表现为内存持续上涨且runtime.NumGoroutine()只增不减;常见原因包括channel未关闭致接收阻塞、ticker未Stop;可通过pprof查看堆栈,检查for range ch是否确保channel关闭,以及ticker是否配对Stop。

goroutine 泄漏的典型表现和快速定位方法
启动大量 goroutine 但程序内存持续上涨、runtime.NumGoroutine() 返回值只增不减,基本可判定存在 goroutine 泄漏。常见原因是 channel 未关闭导致接收方永久阻塞,或 timer/ticker 未 Stop()。
- 用
pprof查看 goroutine 堆栈:curl 'http://localhost:6060/debug/pprof/goroutine?debug=2'
- 检查所有
for range ch循环——必须确保ch会被关闭,否则接收协程永远卡在recv状态 - 所有
time.Ticker必须配对ticker.Stop(),尤其在select+case 场景下
用 sync.WaitGroup 控制固定任务集的并发完成
sync.WaitGroup 适合「已知数量、无需超时、不关心返回值」的并发场景,比如批量写文件、并行初始化服务。它不提供取消或错误传播能力。
- 必须在 goroutine 启动前调用
wg.Add(1),不能在 goroutine 内部调用(竞态风险) -
wg.Done()应放在 defer 中,避免 panic 导致计数器未减少 - 不要在循环里重复声明
var wg sync.WaitGroup——每次都要重置计数器,否则Wait()可能提前返回
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
http.Get(u) // 实际逻辑
}(url)
}
wg.Wait() // 阻塞直到全部完成
用 context.WithTimeout 管理带超时的并发请求
当并发调用外部 API 或数据库,并需要统一超时控制时,context.WithTimeout 是比 time.After 更安全的选择——它能主动取消子 goroutine 的阻塞操作(如 http.Client 支持 context)。
- 不要把同一个
context.Context传给多个独立 goroutine 后再调用cancel()——这会同时中断所有,应为每个 goroutine 创建子 context - HTTP 请求务必使用
http.NewRequestWithContext(ctx, ...),原生http.Get不响应 cancel -
ctx.Err()在超时后为context.DeadlineExceeded,可据此区分超时与其他错误
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Printf("timeout for %s", u)
}
return
}
resp.Body.Close()
}(url)
}
wg.Wait()
用 errgroup.Group 替代手写错误收集与取消
标准库 errgroup.Group 封装了 sync.WaitGroup + context + 错误传播,适用于「并发执行、任一失败即终止、需返回首个错误」的场景,比如微服务多依赖调用。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
eg.Go(func() error { ... })启动任务,不能用go关键字直接起 goroutine - 首次非
nil错误会自动触发ctx.Cancel(),后续 goroutine 应检查ctx.Err()主动退出 - 如果所有 goroutine 都没返回错误,
eg.Wait()返回nil;否则返回第一个非nil错误
g, ctx := errgroup.WithContext(context.Background())
for _, task := range tasks {
task := task // 避免循环变量捕获
g.Go(func() error {
select {
case <-time.After(task.Delay):
return nil
case <-ctx.Done():
return ctx.Err()
}
})
}
if err := g.Wait(); err != nil {
log.Fatal(err) // 第一个出错的任务
}
实际并发控制最易被忽略的是:**goroutine 生命周期与资源释放的耦合关系**。比如开 goroutine 读 channel,却忘了 close;启了 http.Server 却没处理 Shutdown();用了 os.Pipe() 却没 close write end —— 这些都会让 goroutine 挂住,且很难通过日志察觉。










