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

Базовый шаблон sync.WaitGroup: Add до запуска, defer Done внутри, Wait перед чтением результатов. Как избежать гонок при сборе данных.

Короткий ответ

Что ответить на собеседовании

Для ожидания группы задач используют sync.WaitGroup: Add регистрирует работу до запуска горутины, Done отмечает завершение, Wait ожидает обнуления счётчика. Результаты передают отдельно — через канал или безопасно организованное общее хранилище. В заранее выделенном срезе можно дать каждой горутине свой индекс и читать результат после Wait. Сам WaitGroup не защищает конкурентный append или общую map.

Подробный разбор

Ответ с пояснениями

Для группы известных задач подойдёт sync.WaitGroup. Речь о горутинах, а не о выделенном системном потоке для каждой задачи. Сам по себе запуск через go не означает, что вызывающий код дождётся результата.

Базовый шаблон:

package main

import (
    "fmt"
    "sync"
)

func main() {
    input := []int{1, 2, 3}
    results := make([]int, len(input))
    var wg sync.WaitGroup

    for i, value := range input {
        wg.Add(1)
        go func(i, value int) {
            defer wg.Done()
            results[i] = value * value
        }(i, value)
    }

    wg.Wait()
    fmt.Println(results) // [1 4 9]
}

Add(1) выполняется до запуска горутины: иначе ожидающий код может увидеть нулевой счётчик слишком рано. Done должен соответствовать зарегистрированной работе, а Wait возвращается после обнуления счётчика. Использованный WaitGroup нельзя копировать. Правила описаны в документации sync.WaitGroup.

В примере каждая задача записывает в отдельный элемент уже созданного среза. Никто не меняет длину среза и не читает результаты до завершения ожидания. Поэтому порядок элементов соответствует входным данным независимо от порядка выполнения задач. Это не означает, что произвольные конкурентные записи безопасны: общий append, запись в один элемент или обычную map требуют другой организации доступа. Значение синхронизации объясняет модель памяти Go.

Другой вариант — передавать результаты в канал и собирать их одной горутиной. Тогда нужно организовать чтение так, чтобы отправители не заблокировались навсегда до Done.

WaitGroup не собирает ошибки, не отменяет задачи и не ограничивает их количество. Для большого потока работ отдельно задают предел параллелизма; для ошибок и остановки — явный протокол и контекст. time.Sleep не заменяет ожидание завершения.

Практика в реальном времени

Подготовьтесь к следующему собеседованию

Interview Boost учитывает вакансию, резюме и технологии и помогает сформулировать ответ прямо во время интервью.

Начать подготовку