Как корректно остановить горутину в Go? Проблемы с завершением фоновых задач
Основные способы остановки горутины
В Go нет встроенного метода принудительного завершения горутины (как Thread.stop() в Java). Вместо этого используется кооперативная модель: горутина сама должна проверять сигналы остановки. Есть два основных подхода: через канал chan struct{} и через контекст context.Context.
1. Остановка через канал (простой и эффективный)
Создаётся канал, который закрывается, когда нужно остановить горутину. Горутина проверяет закрытие канала с помощью select.
package main
import (
"fmt"
"time"
)
func worker(stop <-chan struct{}) {
for {
select {
case <-stop:
fmt.Println("Worker stopping gracefully")
return
default:
fmt.Println("Working...")
// Имитация долгой операции
time.Sleep(1 * time.Second)
}
}
}
func main() {
stop := make(chan struct{})
go worker(stop)
time.Sleep(3 * time.Second)
close(stop) // Сигнал всем горутинам остановиться
time.Sleep(500 * time.Millisecond) // Даём время на завершение
fmt.Println("Main done")
}
Как это работает:
- Канал
stopпередаётся в горутину как<-chan struct{}(только для чтения). - Внутри цикла
selectпроверяет: если канал закрыт — выходим из функции. - Закрытие канала — это broadcast-сигнал: все горутины, слушающие этот канал, получат нулевое значение.
- Важно: после
close(stop)канал нельзя использовать повторно для новой горутины — нужно создать новый.
2. Остановка через context.Context (рекомендуемый для сложных сценариев)
Пакет context предоставляет стандартный механизм отмены, особенно полезный при работе с HTTP-запросами, базами данных и другими блокирующими операциями.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker cancelled:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(3 * time.Second)
cancel() // Отменяем контекст
time.Sleep(500 * time.Millisecond)
fmt.Println("Main done")
}
Преимущества context:
- Стандартный интерфейс, используемый во многих библиотеках (http, database/sql).
- Поддержка тайм-аутов (
context.WithTimeout) и дедлайнов (context.WithDeadline). - Возможность передачи значений через контекст (но лучше использовать для трейсинга, а не для бизнес-логики).
cancel()можно вызывать несколько раз — безопасно.
3. Остановка с тайм-аутом (если горутина может зависнуть)
Иногда горутина блокируется на операции ввода-вывода, и простой select не срабатывает. В таких случаях используйте контекст с тайм-аутом:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go worker(ctx)
// Ждём завершения или тайм-аута
select {
case <-ctx.Done():
fmt.Println("Operation finished or timed out")
}
Важные правила
- Никогда не используйте
time.Sleepдля синхронизации — это ненадёжно и неэффективно. - Всегда передавайте канал/контекст как параметр, не используйте глобальные переменные.
- Для группы горутин используйте
sync.WaitGroupв паре с каналом/контекстом. - Закрывайте канал только в одном месте (обычно в отправителе), чтобы избежать паники.
В вашем примере с бесконечным циклом достаточно заменить for {} на for { select { ... } } с каналом остановки. Это идиоматичное и безопасное решение.
Альтернативный подход: использование sync.WaitGroup для синхронизации завершения
Если вам нужно дождаться, пока горутина действительно завершится (а не просто отправить сигнал), добавьте sync.WaitGroup. Это особенно полезно при graceful shutdown сервера.
package main
import (
"fmt"
"sync"
"time"
)
func worker(stop <-chan struct{}, wg *sync.WaitGroup) {
defer wg.Done() // Уменьшаем счётчик при выходе
for {
select {
case <-stop:
fmt.Println("Worker finished")
return
default:
fmt.Println("Working...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
stop := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
go worker(stop, &wg)
time.Sleep(3 * time.Second)
close(stop)
wg.Wait() // Ждём реального завершения горутины
fmt.Println("All workers done")
}
Этот подход гарантирует, что горутина завершила все свои операции (например, закрыла файлы или соединения) перед выходом из main. Без WaitGroup программа может завершиться до того, как горутина обработает сигнал остановки.
Также можно комбинировать с контекстом: ctx, cancel := context.WithCancel(...) и передавать ctx.Done() в select. Выбор между каналом и контекстом зависит от сложности проекта: для простых случаев достаточно канала, для сложных — контекст даёт больше гибкости.