> Как запустить две задачи параллельно и дождаться их результата (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: Юрент
Стек: Go
> Пример ответа
Короткий ответ
В Go для параллельного запуска двух задач и ожидания их завершения используется связка go (горутина) и sync.WaitGroup. Альтернатива - каналы с сигнальной семантикой. Для сбора результатов с ошибками лучше использовать errgroup из golang.org/x/sync. Ключевой момент: не разделять общие данные без синхронизации, а передавать результаты через каналы или возвращать из функций в структурах.
Подробное объяснение
Горутины - легковесные потоки, планируемые рантаймом Go. Запуск через go func() не блокирует вызывающий код. sync.WaitGroup предоставляет счётчик: Add(n) перед запуском, Done() при завершении каждой горутины, Wait() блокирует до обнуления счётчика.
Каналы - альтернативный механизм: создаёте буферизированный канал размером 2, каждая горутина отправляет результат, основная функция дважды читает из канала. Это даёт встроенную передачу данных без гонок.
errgroup.Group - надстройка над WaitGroup, которая собирает первую ошибку и отменяет контекст при сбое. Подходит для сценариев, где нужно остановить остальные задачи при ошибке одной.
Важно понимать: параллельность ≠ конкурентность. На многоядерной машине горутины выполняются действительно параллельно, но планировщик может переключать их в любой точке. Поэтому синхронизация доступа к общим данным обязательна.
На практике
Для простого "запустить и дождаться" - WaitGroup + сбор результатов в срез с мьютексом или через канал. Для задач с возможными ошибками - errgroup. Для потоковой передачи промежуточных результатов - каналы с select.
Типичный паттерн для двух независимых задач:
GOfunc runBoth(ctx context.Context) (resultA, resultB Result, err error) {g, ctx := errgroup.WithContext(ctx)var a, b Resultg.Go(func() error {a, err = taskA(ctx)return err})g.Go(func() error {b, err = taskB(ctx)return err})if err := g.Wait(); err != nil {return Result{}, Result{}, err}return a, b, nil}
Если задачи не возвращают ошибок, достаточно sync.WaitGroup и двух переменных, защищённых мьютексом или записываемых до Wait().
Пример кода
GOpackage mainimport ("context""fmt""sync""time")// Вариант 1: WaitGroup + мьютексfunc withWaitGroup() (int, int) {var wg sync.WaitGroupvar mu sync.Mutexvar a, b intwg.Add(2)go func() {defer wg.Done()time.Sleep(100 * time.Millisecond)mu.Lock()a = 42mu.Unlock()}()go func() {defer wg.Done()time.Sleep(50 * time.Millisecond)mu.Lock()b = 24mu.Unlock()}()wg.Wait()return a, b}// Вариант 2: каналыfunc withChannels() (int, int) {ch := make(chan int, 2)go func() {time.Sleep(100 * time.Millisecond)ch <- 42}()go func() {time.Sleep(50 * time.Millisecond)ch <- 24}()a := <-chb := <-chreturn a, b}// Вариант 3: errgroup для задач с ошибкамиfunc withErrGroup(ctx context.Context) (int, int, error) {var a, b intvar errA, errB errorvar wg sync.WaitGroupwg.Add(2)go func() {defer wg.Done()a, errA = taskA(ctx)}()go func() {defer wg.Done()b, errB = taskB(ctx)}()wg.Wait()if errA != nil {return 0, 0, errA}if errB != nil {return 0, 0, errB}return a, b, nil}func taskA(ctx context.Context) (int, error) { return 42, nil }func taskB(ctx context.Context) (int, error) { return 24, nil }func main() {a, b := withWaitGroup()fmt.Println(a, b)a, b = withChannels()fmt.Println(a, b)a, b, err := withErrGroup(context.Background())fmt.Println(a, b, err)}
Как отвечать на собеседовании
Начните с sync.WaitGroup как базового инструмента. Затем упомяните каналы как альтернативу с передачей данных. Для задач с ошибками - errgroup. Покажите понимание разницы между конкурентностью и параллельностью, объясните, почему нельзя просто писать в общие переменные без синхронизации.
Подчеркните, что выбор зависит от сценария: если нужны только результаты - WaitGroup; если нужна передача данных - каналы; если нужна обработка ошибок и отмена - errgroup.
Можно упомянуть context.Context для отмены и таймаутов - это покажет глубину понимания.
Что проверяет интервьюер
- Понимание модели конкурентности в Go (горутины, планировщик).
- Знание примитивов синхронизации:
WaitGroup, мьютексы, каналы. - Умение выбирать правильный инструмент под задачу.
- Понимание проблем гонок данных и способов их избежать.
- Знание
errgroupиcontext- признак реального опыта.
Типичные ошибки
- Забыть
wg.Add(2)перед запуском горутин - приводит к гонке или преждевременному завершению. - Вызов
wg.Addвнутри горутины - небезопасно, счётчик может не успеть увеличиться доWait. - Запись в общие переменные без мьютекса - data race.
- Чтение из канала без проверки закрытия - может заблокировать навсегда.
- Использование
errgroupдля задач без ошибок - избыточно, но не критично. - Игнорирование
context- если задачи долгие, нет способа их отменить.
> Похожие задачи по backend
Что такое embedding
Какой интерфейс реализовать для JSON маршаллинга и анмаршаллинга в Go и почему методы должны иметь ресивер по указателю
Вычистит ли сборщик мусора объекты, если они ссылаются друг на друга, но не имеют ссылок из корня
Что происходит при выполнении функции main с набором принтов и что выводится в консоли
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью