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泄漏 #内存泄漏 #并发安全 #资源管理难度等级: ⭐⭐⭐⭐ 面试频率: 高频
