This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/DEV/GO/Goroutine泄漏排查.md
T

8.3 KiB
Raw Blame History

tags, create time
tags create time
go
golang
runtime
goroutine
泄漏
并发
channel
waitgroup
2026-04-24 10:00

Goroutine 泄漏排查

概述

Goroutine 泄漏是指 goroutine 被创建后,由于代码逻辑问题,永远无法结束,导致其占用的栈内存和其他资源无法回收。Goroutine 是泄漏检测的"隐形杀手"——它不会像进程那样消耗 PID,也不会像内存泄漏那样有明显的 Out Of Memory 错误,而是表现为 runtime.NumGoroutine() 持续增长。

思考:如果一个 goroutine 泄漏了,它会一直占用内存吗?goroutine 初始栈只有 2KB,泄漏 10 万个 goroutine 会占用多少内存?

泄漏场景与排查

场景 1:向无人读的 channel 写

func producer(ch chan<- int) {
    for i := 0; ; i++ {
        ch <- i  // 如果没有接收方,goroutine 永远阻塞
    }
}

func main() {
    ch := make(chan int)
    go producer(ch)  // 泄漏!没有人从 ch 读取
    time.Sleep(1 * time.Second)
    fmt.Println("goroutines:", runtime.NumGoroutine())  // 至少 2 个
}

修复方式:确保有对应的接收方,或者在适当的时候关闭 channel。

func main() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i  // 发送有限数量的数据
        }
        close(ch)  // 发送完毕后关闭
    }()

    // 用 range 消费,channel 关闭后自动退出
    for v := range ch {
        fmt.Println(v)
    }
}

场景 2:读无人写的 channel

func main() {
    ch := make(chan int)
    val := <-ch  // 永远阻塞,goroutine 泄漏
    fmt.Println(val)
}

修复方式:确保有发送方,或者用 select 加超时。

func main() {
    ch := make(chan int)

    select {
    case val := <-ch:
        fmt.Println(val)
    case <-time.After(5 * time.Second):
        fmt.Println("timeout, no data received")
    }
}

场景 3:读写 nil channel

func main() {
    var ch chan int  // nil

    // 以下任何一行都会导致永久阻塞
    // ch <- 1   // 向 nil channel 发送 → 永远阻塞
    // <-ch      // 从 nil channel 接收 → 永远阻塞
    // close(ch) // 关闭 nil channel → panic
}

修复方式:在使用前确保 channel 已初始化。

func main() {
    var ch chan int

    if someCondition {
        ch = make(chan int)
    }

    select {
    case ch <- 1:
        fmt.Println("sent")
    default:
        fmt.Println("channel not initialized, skipped")
    }
}

场景 4:WaitGroup 计数错误

func main() {
    var wg sync.WaitGroup

    // ❌ 错误:在 goroutine 外部调用 wg.Add(1) 是正确的
    // 但在 goroutine 内部调用 Add(1) 会导致 panic
    // 或者忘记调用 wg.Done()

    wg.Add(1)
    go func() {
        defer wg.Done()  // 如果 goroutine 提前 return 忘记调用 Done → 泄漏
        for {
            // 没有退出条件 → goroutine 永远不会结束
        }
    }()

    wg.Wait()  // 永远等待
}

修复方式:确保每个 Add(1) 都有对应的 Done(),且 goroutine 有明确的退出条件。

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            select {
            case <-ctx.Done():
                return  // 有退出条件
            default:
                // 正常处理
            }
        }
    }()

    time.Sleep(1 * time.Second)
    cancel()  // 触发退出
    wg.Wait()
}

场景 5:sync.Mutex 拿锁未释放

var mu sync.Mutex

func leaky() {
    mu.Lock()
    // 如果这里 panic 或者永久阻塞
    // mu.Unlock() 永远不会执行 → 所有等待 mu.Lock() 的 goroutine 泄漏
    panic("boom")  // mu 永远不会被释放
}

func main() {
    go leaky()       // 拿锁后 panic,锁不释放
    go func() {
        mu.Lock()    // 永远等不到锁 → goroutine 泄漏
        mu.Unlock()
    }()

    time.Sleep(1 * time.Second)
    fmt.Println(runtime.NumGoroutine())  // 持续增加
}

修复方式:确保在任何路径下都能释放锁。

func safe() {
    mu.Lock()
    defer mu.Unlock()  // 即使 panic,defer 也会执行
    panic("boom")
}

场景 6:select 中没有默认分支

func main() {
    ch := make(chan int)

    // 如果 ch 没有数据,也没有其他 case 能就绪
    // goroutine 会永远阻塞在 select 上
    select {
    case <-ch:
        fmt.Println("received")
    // 缺少 default 和 time.After → 永远等待
    }
}

泄漏排查工具

1. pprof goroutine profile

# 方法一:导入 net/http/pprof
import _ "net/http/pprof"

# 方法二:使用 runtime/pprof
import (
    "runtime"
    "runtime/pprof"
    "os"
)

func dumpGoroutines() {
    f, _ := os.Create("goroutine.prof")
    pprof.WriteProfile(f)
    f.Close()
}
# 分析 goroutine 泄漏
go tool pprof goroutine.prof

# 进入交互界面后执行:
top          # 查看最顶层的 goroutine
top -cum     # 按累积时间排序
list main.leaky  # 查看具体函数的 goroutine 阻塞位置

2. 监控 NumGoroutine

import (
    "runtime"
    "time"
)

func monitorGoroutines() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        n := runtime.NumGoroutine()
        if n > 100 {  // 设定阈值
            fmt.Printf("⚠️  High goroutine count: %d\n", n)
            //  dump stack
            buf := make([]byte, 1<<20)
            n := runtime.Stack(buf, true)
            fmt.Printf("Goroutine stacks:\n%s\n", buf[:n])
        }
    }
}

3. pprof 常用命令速查

# 启动 web UI(推荐)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine?debug=1

# 文本模式
go tool pprof -text goroutine.prof

# 查看具体函数阻塞位置
go tool pprof -list=main.worker goroutine.prof

预防最佳实践

实践 说明
每个 goroutine 必须有退出条件 用 context.Context 控制生命周期
Channel 用完必须关闭 发送方负责关闭,防止接收方永远等待
WaitGroup 配对使用 Add 和 Done 一一对应,用 defer 确保 Done 执行
Mutex 用 defer 释放 defer mu.Unlock() 防止 panic 导致锁不释放
select 加超时 永远不要 select 中只有阻塞 channel 而无 default/timeout
启动时加监控 定期采样 runtime.NumGoroutine(),异常时 dump stack
context 传递取消信号 让所有 goroutine 都能响应取消
// ✅ 推荐的 goroutine 模板
func worker(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()

    for {
        select {
        case <-ctx.Done():
            return  // 正常退出
        default:
            // 执行任务
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(ctx, &wg)
    }

    wg.Wait()
}

排查流程图

flowchart TD
    A["发现 goroutine 持续增长"] --> B{"NumGoroutine 持续增长?"}
    B -->|"是"| C["go tool pprof goroutine profile"]
    C --> D["top / top -cum 查看类型分布"]
    D --> E{"泄漏的 goroutine 类型?"}

    E -->|"chan send / chan receive"| F["检查 channel 发送方/接收方配对"]
    F --> G["确保有对应的读取/写入"]
    G --> H["使用 select + timeout 防止永久等待"]

    E -->|"sync.Mutex lock"| I["检查 mutex 是否被正确释放"]
    I --> J["所有路径都使用 defer Unlock"]

    E -->|"sleep / IO"| K["检查是否有阻塞的 IO 操作"]
    K --> L["确保有超时和取消机制"]

    E -->|"goroutine 数量少且稳定"| M["可能是正常的工作 goroutine"]
    M --> N["确认是否为预期行为"]

    B -->|"否"| O["没有泄漏,goroutine 数量正常"]

    classDef leak fill:#e53935,color:#fff
    classDef fix fill:#43a047,color:#fff
    classDef normal fill:#1e88e5,color:#fff
    class C,D,F,I,K,M,O normal
    class G,H,J,L fix
    class A leak

关联笔记