Skip to content

Goroutine 泄漏

Goroutine 泄漏是 Go 程序中常见的内存泄漏问题,会导致程序资源消耗不断增长。

泄漏检测

基础检测方法

问题: 如何检测 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func detectGoroutineLeak() {
    fmt.Println("=== Goroutine 泄漏检测 ===")
    
    before := runtime.NumGoroutine()
    fmt.Printf("操作前 Goroutine 数量: %d\n", before)
    
    // 创建可能泄漏的 goroutine
    createLeakyGoroutines()
    
    // 等待一段时间
    time.Sleep(100 * time.Millisecond)
    
    after := runtime.NumGoroutine()
    fmt.Printf("操作后 Goroutine 数量: %d\n", after)
    
    if after > before {
        fmt.Printf("检测到 Goroutine 泄漏: 增加了 %d\n", after-before)
    }
}

func createLeakyGoroutines() {
    // 这些 goroutine 会泄漏
    for i := 0; i < 10; i++ {
        go func() {
            // 永远阻塞的操作
            <-make(chan struct{})
        }()
    }
}

func main() {
    detectGoroutineLeak()
}

:::

常见泄漏模式

无缓冲通道泄漏

问题: 无缓冲通道如何导致 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
    "context"
    "sync"
)

// 泄漏模式1:发送端无接收端
func leakyChannelSender() {
    fmt.Println("\n=== 无缓冲通道泄漏 - 发送端 ===")
    
    ch := make(chan int)
    
    // 这个goroutine会永远阻塞
    go func() {
        ch <- 42 // 没有接收端,永远阻塞
        fmt.Println("这行代码永远不会执行")
    }()
    
    // 主goroutine没有从ch接收数据
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("当前 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

// 修复方案1:使用带缓冲的通道
func fixedChannelSender() {
    fmt.Println("\n=== 修复方案 - 带缓冲通道 ===")
    
    ch := make(chan int, 1) // 缓冲大小为1
    
    go func() {
        ch <- 42 // 不会阻塞
        fmt.Println("发送完成")
    }()
    
    time.Sleep(100 * time.Millisecond)
    select {
    case value := <-ch:
        fmt.Printf("接收到值: %d\n", value)
    default:
        fmt.Println("没有值可接收")
    }
}

// 修复方案2:使用context控制
func fixedWithContext() {
    fmt.Println("\n=== 修复方案 - Context控制 ===")
    
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    
    ch := make(chan int)
    
    go func() {
        select {
        case ch <- 42:
            fmt.Println("成功发送")
        case <-ctx.Done():
            fmt.Println("发送超时,goroutine退出")
            return
        }
    }()
    
    select {
    case value := <-ch:
        fmt.Printf("接收到值: %d\n", value)
    case <-ctx.Done():
        fmt.Println("接收超时")
    }
    
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("当前 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

::: :::

HTTP 请求泄漏

问题: HTTP 请求如何导致 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
import (
    "io"
    "net/http"
)

// 泄漏模式:未正确关闭HTTP响应
func leakyHTTPRequest() {
    fmt.Println("\n=== HTTP请求泄漏 ===")
    
    for i := 0; i < 5; i++ {
        go func(id int) {
            resp, err := http.Get("https://httpbin.org/delay/1")
            if err != nil {
                fmt.Printf("请求 %d 失败: %v\n", id, err)
                return
            }
            
            // 错误:没有关闭响应体
            // defer resp.Body.Close()
            
            fmt.Printf("请求 %d 完成\n", id)
        }(i)
    }
    
    time.Sleep(2 * time.Second)
    fmt.Printf("HTTP请求后 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

// 修复方案:正确处理HTTP响应
func fixedHTTPRequest() {
    fmt.Println("\n=== 修复的HTTP请求 ===")
    
    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
            defer cancel()
            
            req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/1", nil)
            if err != nil {
                fmt.Printf("创建请求 %d 失败: %v\n", id, err)
                return
            }
            
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                fmt.Printf("请求 %d 失败: %v\n", id, err)
                return
            }
            
            // 正确:关闭响应体
            defer resp.Body.Close()
            
            // 读取并丢弃响应体
            io.Copy(io.Discard, resp.Body)
            
            fmt.Printf("请求 %d 完成\n", id)
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("修复后 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

::: :::

定时器泄漏

问题: 定时器如何导致 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 泄漏模式:未停止的定时器
func leakyTimer() {
    fmt.Println("\n=== 定时器泄漏 ===")
    
    for i := 0; i < 10; i++ {
        go func(id int) {
            // 错误:定时器没有被停止
            timer := time.NewTimer(time.Hour)
            
            select {
            case <-timer.C:
                fmt.Printf("定时器 %d 触发\n", id)
            case <-time.After(100 * time.Millisecond):
                fmt.Printf("定时器 %d 提前退出\n", id)
                // 错误:没有停止定时器
                // timer.Stop()
                return
            }
        }(i)
    }
    
    time.Sleep(200 * time.Millisecond)
    fmt.Printf("定时器泄漏后 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

// 修复方案:正确停止定时器
func fixedTimer() {
    fmt.Println("\n=== 修复的定时器 ===")
    
    var wg sync.WaitGroup
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            timer := time.NewTimer(time.Hour)
            defer timer.Stop() // 确保定时器被停止
            
            select {
            case <-timer.C:
                fmt.Printf("定时器 %d 触发\n", id)
            case <-time.After(100 * time.Millisecond):
                fmt.Printf("定时器 %d 提前退出\n", id)
                return
            }
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("修复后 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

::: :::

泄漏预防

Context 模式

问题: 如何使用 Context 防止 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
// 通用的goroutine管理模式
func contextBasedGoroutineManagement() {
    fmt.Println("\n=== Context管理Goroutine ===")
    
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保所有goroutine退出
    
    var wg sync.WaitGroup
    
    // 启动工作goroutine
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(ctx, &wg, i)
    }
    
    // 模拟一些工作时间
    time.Sleep(500 * time.Millisecond)
    
    // 取消所有工作
    cancel()
    
    // 等待所有goroutine完成
    wg.Wait()
    
    fmt.Printf("所有工作完成,Goroutine 数量: %d\n", runtime.NumGoroutine())
}

func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
    defer wg.Done()
    
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d 收到取消信号,退出\n", id)
            return
        case <-ticker.C:
            fmt.Printf("Worker %d 正在工作\n", id)
        }
    }
}

::: :::

生产者-消费者模式

问题: 如何在生产者-消费者模式中避免泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
点击查看完整代码实现
go
type SafeProducerConsumer struct {
    dataChan chan int
    ctx      context.Context
    cancel   context.CancelFunc
    wg       sync.WaitGroup
}

func NewSafeProducerConsumer() *SafeProducerConsumer {
    ctx, cancel := context.WithCancel(context.Background())
    
    return &SafeProducerConsumer{
        dataChan: make(chan int, 10),
        ctx:      ctx,
        cancel:   cancel,
    }
}

func (spc *SafeProducerConsumer) Start() {
    // 启动生产者
    spc.wg.Add(1)
    go spc.producer()
    
    // 启动消费者
    spc.wg.Add(1)
    go spc.consumer()
}

func (spc *SafeProducerConsumer) Stop() {
    spc.cancel()
    spc.wg.Wait()
}

func (spc *SafeProducerConsumer) producer() {
    defer spc.wg.Done()
    defer close(spc.dataChan)
    
    ticker := time.NewTicker(50 * time.Millisecond)
    defer ticker.Stop()
    
    counter := 0
    for {
        select {
        case <-spc.ctx.Done():
            fmt.Println("生产者退出")
            return
        case <-ticker.C:
            select {
            case spc.dataChan <- counter:
                fmt.Printf("生产: %d\n", counter)
                counter++
            case <-spc.ctx.Done():
                fmt.Println("生产者退出")
                return
            }
        }
    }
}

func (spc *SafeProducerConsumer) consumer() {
    defer spc.wg.Done()
    
    for {
        select {
        case <-spc.ctx.Done():
            fmt.Println("消费者退出")
            return
        case data, ok := <-spc.dataChan:
            if !ok {
                fmt.Println("数据通道关闭,消费者退出")
                return
            }
            fmt.Printf("消费: %d\n", data)
        }
    }
}

func safeProducerConsumerExample() {
    fmt.Println("\n=== 安全的生产者-消费者 ===")
    
    spc := NewSafeProducerConsumer()
    spc.Start()
    
    // 运行一段时间
    time.Sleep(300 * time.Millisecond)
    
    // 安全停止
    spc.Stop()
    
    fmt.Printf("停止后 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

::: :::

泄漏监控

实时监控

问题: 如何实时监控 Goroutine 泄漏?

回答:

点击查看完整代码实现
点击查看完整代码实现
go
type GoroutineMonitor struct {
    threshold   int
    checkPeriod time.Duration
    alertChan   chan string
    ctx         context.Context
    cancel      context.CancelFunc
}

func NewGoroutineMonitor(threshold int, checkPeriod time.Duration) *GoroutineMonitor {
    ctx, cancel := context.WithCancel(context.Background())
    
    return &GoroutineMonitor{
        threshold:   threshold,
        checkPeriod: checkPeriod,
        alertChan:   make(chan string, 10),
        ctx:         ctx,
        cancel:      cancel,
    }
}

func (gm *GoroutineMonitor) Start() {
    go gm.monitor()
    go gm.handleAlerts()
}

func (gm *GoroutineMonitor) Stop() {
    gm.cancel()
}

func (gm *GoroutineMonitor) monitor() {
    ticker := time.NewTicker(gm.checkPeriod)
    defer ticker.Stop()
    
    baselineCount := runtime.NumGoroutine()
    
    for {
        select {
        case <-gm.ctx.Done():
            return
        case <-ticker.C:
            currentCount := runtime.NumGoroutine()
            
            if currentCount > baselineCount+gm.threshold {
                alert := fmt.Sprintf("Goroutine 数量异常: 当前 %d, 基线 %d, 阈值 %d", 
                    currentCount, baselineCount, gm.threshold)
                
                select {
                case gm.alertChan <- alert:
                default:
                    // 如果alert channel满了,跳过这次警报
                }
            }
            
            // 动态调整基线
            if currentCount < baselineCount {
                baselineCount = currentCount
            }
        }
    }
}

func (gm *GoroutineMonitor) handleAlerts() {
    for {
        select {
        case <-gm.ctx.Done():
            return
        case alert := <-gm.alertChan:
            fmt.Printf("🚨 ALERT: %s\n", alert)
            
            // 可以在这里添加其他处理逻辑:
            // - 发送通知
            // - 记录日志
            // - 触发堆栈dump
            gm.dumpGoroutineStacks()
        }
    }
}

func (gm *GoroutineMonitor) dumpGoroutineStacks() {
    buf := make([]byte, 1024*1024) // 1MB buffer
    stackSize := runtime.Stack(buf, true)
    
    fmt.Printf("Goroutine 堆栈信息 (前1000字符):\n%s\n", 
        string(buf[:min(1000, stackSize)]))
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

// 监控使用示例
func monitoringExample() {
    fmt.Println("\n=== Goroutine监控示例 ===")
    
    monitor := NewGoroutineMonitor(5, time.Second)
    monitor.Start()
    defer monitor.Stop()
    
    // 故意创建一些泄漏的goroutine
    for i := 0; i < 10; i++ {
        go func() {
            time.Sleep(time.Hour) // 模拟泄漏
        }()
    }
    
    // 让监控器运行一段时间
    time.Sleep(3 * time.Second)
}

func main() {
    detectGoroutineLeak()
    leakyChannelSender()
    fixedChannelSender()
    fixedWithContext()
    //leakyHTTPRequest()
    //fixedHTTPRequest()
    leakyTimer()
    fixedTimer()
    contextBasedGoroutineManagement()
    safeProducerConsumerExample()
    monitoringExample()
}

:::

技术标签: #Goroutine泄漏 #内存泄漏 #并发安全 #资源管理难度等级: ⭐⭐⭐⭐ 面试频率: 高频

正在精进