Goroutine 泄露
🔴 困难题目描述
以下代码存在什么问题?如何修复?
func fetchData(ch chan<- int) {
// 模拟耗时操作
time.Sleep(time.Second)
ch <- 42
}
func process() {
ch := make(chan int)
go fetchData(ch)
// 如果这里提前返回或出错,goroutine 会泄露
if someCondition {
return
}
result := <-ch
fmt.Println(result)
}提示
- goroutine 泄露是指 goroutine 无法退出,一直占用资源
- channel 阻塞是常见原因
- 使用 context 可以控制 goroutine 生命周期
解法
参考答案 (3 个标签)
goroutine context channel
问题分析
原代码的问题:
- goroutine 永久阻塞:如果
process()提前返回,ch没有接收者,fetchData中的ch <- 42会永久阻塞 - 资源泄露:goroutine 占用的内存和栈空间无法释放
- 难以发现:不会立即 panic,而是逐渐消耗内存
修复方案
func fetchData(ctx context.Context, ch chan<- int) {
result := doWork() // 模拟耗时操作
select {
case <-ctx.Done():
fmt.Println("goroutine cancelled")
return
case ch <- result:
fmt.Println("result sent")
}
}
func process() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保取消
ch := make(chan int)
go fetchData(ctx, ch)
if someCondition {
return errors.New("early return")
}
result := <-ch
fmt.Println(result)
return nil
}常见泄露场景
1. Channel 阻塞
// ❌ 泄露:无接收者
func leak() {
ch := make(chan int)
go func() {
ch <- 1 // 永远阻塞
}()
}
// ✅ 修复:使用 context
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case ch <- 1:
case <-ctx.Done():
return
}
}()
}2. 无限循环
// ❌ 泄露:无限循环
func leak() {
go func() {
for {
time.Sleep(time.Second)
// 没有退出条件
}
}()
}
// ✅ 修复:添加退出条件
func noLeak(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 处理任务
case <-ctx.Done():
return
}
}
}()
}3. Select 无 Default
// ❌ 泄露:所有 case 都阻塞
func leak() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
select {
case <-ch1: // 无发送者
case <-ch2: // 无发送者
}
// 永久阻塞
}()
}
// ✅ 修复:添加 default 或 context
func noLeak(ctx context.Context) {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
select {
case <-ch1:
case <-ch2:
case <-ctx.Done():
return
}
}()
}检测 Goroutine 泄露
// 方法 1:运行时统计
func checkGoroutineCount() {
fmt.Println(runtime.NumGoroutine())
}
// 方法 2:HTTP pprof
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 访问 http://localhost:6060/debug/pprof/goroutine?debug=2
}
// 方法 3:测试前后对比
func TestNoLeak(t *testing.T) {
before := runtime.NumGoroutine()
// 执行测试代码
process()
after := runtime.NumGoroutine()
if after != before {
t.Errorf("goroutine leak detected: %d -> %d", before, after)
}
}