惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
L
LangChain Blog
Y
Y Combinator Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
博客园 - Franky
V
Visual Studio Blog
小众软件
小众软件
月光博客
月光博客
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
美团技术团队
量子位

博客园 - 若-飞

电商订单为什么要存「交易快照」 Git Submodule Sync 完整技术文档 企业AI Agent落地的核心逻辑与路径 基于langchain,Function Call的成功率怎么解决? LangChain Checkpoint(检查点)是什么?—— Agent 的"存档机制" RAG 设计:Embedding 如何切分 AI 客服系统设计:RAG 知识库设计 Go 百万连接服务器设计:从网卡到业务的全链路解析 深度解析 sync.Pool:从设计哲学到生产实践 Go Channel 关闭与超时机制完全指南 Go Map 无限增长问题解决方案 LangChain 聊天记录压缩:原理、机制与实战 揭开 sklearn 文本分类的核心原理:从词袋到逻辑回归 大模型“胡说八道”怎么办?一张图读懂检测、评估与修复全方案 企业级AI知识库权限隔离设计:让AI“懂规矩”比“懂知识”更重要 RAG系统设计全解析:从架构到多模态的核心知识图谱 RAG召回率提升全攻略:7大核心方法让检索更精准 RAG召回率提升秘籍:Metadata过滤的底层原理与实践 构建更好的RAG系统:深入理解混合搜索 一文搞懂 RAG 中 Retriever 和 Reranker 的区别 一文搞懂 RAG 的召回率(Recall)是什么? LangChain / LangGraph、MCP、Harness Engineer 与 Claude Code 的对应关系 Agent Harness 技术笔记:从 Trajectory 到 Function Calling Loop BLEU 是什么?——从原理到工程实践 一文讲清:Approve / Permit / Permit2 的本质区别 分库分表后跨分页查询的完整方案 ai如何处理私有数据 ai幻觉是啥,以及如何解决 别再让大模型“凭空瞎猜”了!带你认识AI最强外挂:ChromaDB 用 useQuery 管请求:TanStack React Query 入门小结
Goroutine 泄漏:原因、检测与防范
若-飞 · 2026-07-12 · via 博客园 - 若-飞

Goroutine 泄漏:原因、检测与防范

防止 Goroutine 泄漏的核心是:每个 goroutine 都应有明确的退出路径,使用 context 控制生命周期,用 select 加超时保护 channel 操作,用 WaitGroup 等待完成,并持续监控数量变化。


一、什么是 Goroutine 泄漏?

Goroutine 泄漏是指 goroutine 因永久阻塞而无法正常退出,一直占用内存和资源,最终导致程序 OOM 或性能下降。

常见泄漏场景

// ❌ 场景1:从空 channel 接收
ch := make(chan int)
data := <-ch  // 永远阻塞(没人发送)

// ❌ 场景2:向已满的 channel 发送
ch := make(chan int, 1)
ch <- 1
ch <- 2  // 永远阻塞(没消费者)

// ❌ 场景3:等待不会发生的关闭
for v := range ch {  // 永远等不到 close
    fmt.Println(v)
}

// ❌ 场景4:goroutine 死循环无退出
go func() {
    for {
        doWork()  // 永远运行,无法停止
    }
}()

二、核心解决方案

1. 使用 Context 控制生命周期(推荐)

func worker(ctx context.Context, ch <-chan int) {
    for {
        select {
        case data, ok := <-ch:
            if !ok {
                return
            }
            process(data)
        case <-ctx.Done():
            fmt.Println("worker 退出:", ctx.Err())
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    ch := make(chan int)
    go worker(ctx, ch)
    
    ch <- 1
    ch <- 2
    // 5秒后自动退出
}

2. 使用 WaitGroup 确保完成

func main() {
    var wg sync.WaitGroup
    ch := make(chan int, 10)
    
    // 生产者
    wg.Add(1)
    go func() {
        defer wg.Done()
        defer close(ch)
        for i := 0; i < 5; i++ {
            ch <- i
        }
    }()
    
    // 消费者
    wg.Add(1)
    go func() {
        defer wg.Done()
        for data := range ch {
            fmt.Println(data)
        }
    }()
    
    wg.Wait()  // 等待所有 goroutine 完成
    fmt.Println("所有 goroutine 已退出")
}

3. 所有 Channel 操作用 Select 保护

// ✅ 安全发送
func safeSend(ch chan<- int, data int, ctx context.Context) error {
    select {
    case ch <- data:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// ✅ 安全接收
func safeReceive(ch <-chan int, ctx context.Context) (int, error) {
    select {
    case data, ok := <-ch:
        if !ok {
            return 0, fmt.Errorf("channel 已关闭")
        }
        return data, nil
    case <-ctx.Done():
        return 0, ctx.Err()
    }
}

4. 监控 Goroutine 数量

func monitor() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        count := runtime.NumGoroutine()
        fmt.Printf("当前 goroutine 数量: %d\n", count)
        
        if count > 100 {
            log.Printf("警告: goroutine 数量过多 (%d)", count)
            // 发送告警
        }
    }
}

三、完整示例:安全的 Worker Pool

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type Job struct {
    ID int
}

type Pool struct {
    jobCh    chan Job
    ctx      context.Context
    cancel   context.CancelFunc
    wg       sync.WaitGroup
    closeOnce sync.Once
}

func NewPool(workers, queueSize int) *Pool {
    ctx, cancel := context.WithCancel(context.Background())
    p := &Pool{
        jobCh:  make(chan Job, queueSize),
        ctx:    ctx,
        cancel: cancel,
    }
    
    for i := 0; i < workers; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }
    return p
}

func (p *Pool) worker(id int) {
    defer p.wg.Done()
    
    for {
        select {
        case job, ok := <-p.jobCh:
            if !ok {
                return
            }
            fmt.Printf("Worker %d 处理 Job %d\n", id, job.ID)
            time.Sleep(100 * time.Millisecond)
            
        case <-p.ctx.Done():
            fmt.Printf("Worker %d 退出\n", id)
            return
        }
    }
}

func (p *Pool) Submit(job Job) bool {
    select {
    case p.jobCh <- job:
        return true
    case <-p.ctx.Done():
        return false
    default:
        fmt.Println("队列满,丢弃 Job:", job.ID)
        return false
    }
}

func (p *Pool) Shutdown(timeout time.Duration) {
    p.closeOnce.Do(func() {
        close(p.jobCh)
        
        done := make(chan struct{})
        go func() {
            p.wg.Wait()
            close(done)
        }()
        
        select {
        case <-done:
            fmt.Println("正常关闭")
        case <-time.After(timeout):
            fmt.Println("超时,强制关闭")
            p.cancel()
        }
    })
}

func main() {
    pool := NewPool(3, 5)
    
    // 提交任务
    for i := 0; i < 20; i++ {
        pool.Submit(Job{ID: i})
    }
    
    // 优雅关闭
    pool.Shutdown(2 * time.Second)
    fmt.Println("程序结束")
}

四、快速自查清单

□ 每个 goroutine 都有退出路径吗?
□ 使用了 context 控制生命周期吗?
□ channel 操作用 select 包裹了吗?
□ 有超时保护吗(time.After)?
□ 使用 WaitGroup 等待完成吗?
□ 监控 goroutine 数量变化吗?
□ 确保谁创建谁关闭 channel 吗?
□ 使用 sync.Once 防止重复关闭吗?

五、总结

方案 用途 关键代码
Context 生命周期控制 ctx.Done()
Select 非阻塞/超时操作 select { case ... }
WaitGroup 等待完成 wg.Add/Done/Wait
超时 防止永久阻塞 time.After
监控 提前发现问题 runtime.NumGoroutine()

核心原则

每个 goroutine 都必须知道"何时退出",而不是"是否会退出"。 使用 context、select、WaitGroup 组合,让退出路径清晰可控,并持续监控数量变化。