Ответ
Правильное управление жизненным циклом горутин — ключевой аспект конкурентного программирования в 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 воркеров и дождаться, пока все закончат".