← Назад к вопросам

Найти баг: Goroutine Leak

2.0 Middle🔥 241 комментариев
#Конкурентность и горутины#Производительность и оптимизация

Условие

Найдите проблему утечки горутин в следующем коде:

package main

import (
    "fmt"
    "time"
)

func process() <-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; ; i++ {
            ch <- i
            time.Sleep(100 * time.Millisecond)
        }
    }()
    return ch
}

func main() {
    ch := process()
    for i := 0; i < 5; i++ {
        fmt.Println(<-ch)
    }
    fmt.Println("done")
}

Вопросы

  1. Какая проблема в этом коде?
  2. Как исправить код, чтобы горутина корректно завершалась?
  3. Покажите решение с использованием context

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Это классическая задача на goroutine leak — утечку горутин. Горутина остаётся запущенной даже после того, как её результаты больше не нужны.

1. Какая проблема?

Проблема: Goroutine Leak (утечка горутин)

Горотина запущенная в process() никогда не завершается:

for i := 0; ; i++ {  // ← бесконечный цикл
    ch <- i
    time.Sleep(100 * time.Millisecond)
}

В main читаются только 5 значений, затем программа выводит "done" и завершается, но горутина остаётся в памяти до выхода программы.

Последствия:

  • Утечка памяти (горутина занимает память)
  • Утечка системных ресурсов
  • В больших системах может привести к exhaust всех горутин

2. Как исправить?

Вариант 1: Закрыть канал

func process(count int) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)  // ← гарантирует закрытие
        for i := 0; i < count; i++ {
            ch <- i
            time.Sleep(100 * time.Millisecond)
        }
    }()
    return ch
}

func main() {
    ch := process(5)
    for val := range ch {  // ← range автоматически завершится
        fmt.Println(val)
    }
    fmt.Println("done")
}

Вариант 2: Передать количество

func process(count int) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        for i := 0; i < count; i++  // ← конечный цикл
            ch <- i
            time.Sleep(100 * time.Millisecond)
        }
    }()
    return ch
}

3. Решение с context (РЕКОМЕНДУЕТСЯ)

Это самый правильный способ:

package main

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

func process(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)  // гарантирует закрытие канала
        i := 0
        for {
            select {
            case <-ctx.Done():  // ← проверяем отмену
                return  // горутина корректно завершается
            case ch <- i:
                i++
                time.Sleep(100 * time.Millisecond)
            }
        }
    }()
    return ch
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()  // гарантирует отмену
    
    ch := process(ctx)
    for i := 0; i < 5; i++ {
        fmt.Println(<-ch)
    }
    
    cancel()  // ← отменяем контекст, горутина завершается
    time.Sleep(200 * time.Millisecond)  // ждём завершения
    fmt.Println("done")
}

Почему это лучше:

  • Явно сигнализирует горутине о завершении
  • Работает с таймаутами
  • Cascading cancellation в иерархии
  • Production-ready подход

Полное решение с context

package main

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

func process(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        
        i := 0
        ticker := time.NewTicker(100 * time.Millisecond)
        defer ticker.Stop()  // гарантирует остановку ticker
        
        for {
            select {
            case <-ctx.Done():  // отмена контекста
                fmt.Println("Горутина завершена")
                return
            case <-ticker.C:  // каждые 100ms
                select {
                case ch <- i:  // отправить значение
                    i++
                case <-ctx.Done():  // проверить ещё раз
                    return
                }
            }
        }
    }()
    return ch
}

func main() {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        1 * time.Second,  // таймаут 1 секунда
    )
    defer cancel()
    
    ch := process(ctx)
    
    for val := range ch {
        fmt.Println("Значение:", val)
        if val >= 4 {
            break  // выходим после 5 значений
        }
    }
    
    cancel()  // отменяем
    time.Sleep(200 * time.Millisecond)  // ждём завершения
    fmt.Println("done")
}
Найти баг: Goroutine Leak | PrepBro