Как дождаться корректного завершения группы горутин?

Ответ

Для синхронизации и ожидания завершения горутин в Go используются несколько основных подходов.

  1. sync.WaitGroup — основной и идиоматичный способ WaitGroup — это счетчик активных горутин. Он идеально подходит, когда нужно запустить несколько независимых горутин и дождаться, пока все они завершат свою работу.

    var wg sync.WaitGroup
    
    for i := 0; i < 5; i++ {
        wg.Add(1) // Увеличиваем счетчик перед запуском горутины
        go func(workerID int) {
            defer wg.Done() // Уменьшаем счетчик при завершении
            fmt.Printf("Воркер %d завершил работуn", workerID)
        }(i)
    }
    
    wg.Wait() // Блокирует выполнение, пока счетчик не станет равен нулю
    fmt.Println("Все воркеры завершили работу")
  2. Каналы для сигнализации Каналы можно использовать для сигнализации о завершении, но для группы горутин это менее удобно, чем WaitGroup. Этот способ хорошо подходит для координации между двумя горутинами или когда нужно передать результат.

    done := make(chan bool)
    go func() {
        // ... какая-то работа ...
        done <- true // Отправляем сигнал о завершении
    }()
    
    <-done // Ожидаем сигнала
  3. context для отмены операций Контекст используется не для ожидания, а для сигнализации об отмене. Горутины должны слушать канал ctx.Done() и прекращать работу, когда он закрывается. Чтобы дождаться фактического завершения после отмены, context часто используют вместе с WaitGroup.

    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())
    
    wg.Add(1)
    go worker(ctx, &wg)
    
    // ... спустя какое-то время решаем остановить воркер
    cancel() // Отправляем сигнал отмены
    wg.Wait() // Ждем, пока горутина корректно завершится
  4. errgroup для групп горутин с обработкой ошибок Пакет golang.org/x/sync/errgroup предоставляет удобную абстракцию над WaitGroup для случаев, когда нужно дождаться завершения группы горутин и обработать первую возникшую ошибку.

    g, ctx := errgroup.WithContext(context.Background())
    
    for _, url := range urls {
        url := url // https://golang.org/doc/faq#closures_and_goroutines
        g.Go(func() error {
            // Эта горутина будет запущена в группе
            resp, err := http.Get(url)
            if err != nil {
                return err // Возвращаем ошибку
            }
            resp.Body.Close()
            return nil
        })
    }
    
    // Ждем завершения всех горутин. Если одна из них вернет ошибку,
    // err будет не nil, и контекст g будет отменен для остальных.
    if err := g.Wait(); err != nil {
        log.Printf("Произошла ошибка: %v", err)
    }

Вывод: Для простого ожидания используйте sync.WaitGroup. Для отмены — context + WaitGroup. Для обработки ошибок в группе — errgroup.