Какие существуют способы управления жизненным циклом горутины?

Ответ

Правильное управление жизненным циклом горутин — ключевой аспект конкурентного программирования в Go для предотвращения утечек ресурсов и гонок данных. Основное правило: никогда не запускайте горутину, не зная, как она будет остановлена.

Существует три основных паттерна:

1. Канал для отмены (Done Channel)

Это базовый и простой способ. В горутину передается канал, закрытие которого служит сигналом для завершения работы.

done := make(chan struct{}) // Используем пустую структуру, т.к. она не занимает памяти

go func() {
    for {
        select {
        case <-done: // Канал закрыт, получаем "нулевое" значение
            fmt.Println("Горутина получила сигнал завершения.")
            return
        default:
            // Выполнение полезной работы
            time.Sleep(500 * time.Millisecond)
        }
    }
}()

// Через некоторое время даем сигнал на остановку
time.Sleep(2 * time.Second)
close(done) // Закрытие канала — широковещательный сигнал для всех, кто его слушает

2. Использование context.Context (Идиоматичный способ)

Пакет context — это стандартный и наиболее гибкий механизм для отмены операций, установки дедлайнов и передачи request-scoped данных. Он является развитием идеи Done Channel.

// WithCancel возвращает контекст и функцию для его отмены
ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
    for {
        select {
        case <-ctx.Done(): // Канал Done() закроется при вызове cancel()
            fmt.Println("Горутина завершается по контексту.")
            return
        default:
            // Полезная работа
            time.Sleep(500 * time.Millisecond)
        }
    }
}(ctx)

// Даем сигнал на остановку
time.Sleep(2 * time.Second)
cancel() // Вызов функции отмены

3. Использование sync.WaitGroup для ожидания завершения

WaitGroup не используется для сигнализации об отмене, а для ожидания завершения группы горутин. Его часто используют совместно с context или done каналом.

var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())

wg.Add(1) // Увеличиваем счетчик горутин
go func(ctx context.Context) {
    defer wg.Done() // Уменьшаем счетчик при выходе из горутины
    // ... логика с select <-ctx.Done() ...
    fmt.Println("Горутина завершила работу и вызвала Done().")
}(ctx)

cancel() // Сигнализируем об отмене
wg.Wait() // Блокирует выполнение, пока счетчик WaitGroup не станет равен нулю
fmt.Println("Все горутины завершились.")

Когда что использовать?

  • Done Channel: Для простых сценариев, где не нужны таймауты или передача значений.
  • context.Context: Предпочтительный способ в большинстве случаев, особенно в сетевых сервисах. Он поддерживает таймауты (WithTimeout), дедлайны (WithDeadline) и иерархическую отмену.
  • sync.WaitGroup: Только для ожидания завершения. Идеально подходит для сценариев "запустить N воркеров и дождаться, пока все закончат".