Для ожидания группы задач используют sync.WaitGroup: Add регистрирует работу до запуска горутины, Done отмечает завершение, Wait ожидает обнуления счётчика. Результаты передают отдельно — через канал или безопасно организованное общее хранилище. В заранее выделенном срезе можно дать каждой горутине свой индекс и читать результат после Wait. Сам WaitGroup не защищает конкурентный append или общую map.
Как в Go дождаться завершения всех запущенных горутин и использовать результаты?
Базовый шаблон sync.WaitGroup: Add до запуска, defer Done внутри, Wait перед чтением результатов. Как избежать гонок при сборе данных.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Для группы известных задач подойдёт 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 не заменяет ожидание завершения.