> Как запустить две задачи параллельно и дождаться их результата (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.

Типичный паттерн для двух независимых задач:

GO
func runBoth(ctx context.Context) (resultA, resultB Result, err error) {
g, ctx := errgroup.WithContext(ctx)
var a, b Result
g.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().

Пример кода

GO
package main
import (
"context"
"fmt"
"sync"
"time"
)
// Вариант 1: WaitGroup + мьютекс
func withWaitGroup() (int, int) {
var wg sync.WaitGroup
var mu sync.Mutex
var a, b int
wg.Add(2)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond)
mu.Lock()
a = 42
mu.Unlock()
}()
go func() {
defer wg.Done()
time.Sleep(50 * time.Millisecond)
mu.Lock()
b = 24
mu.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 := <-ch
b := <-ch
return a, b
}
// Вариант 3: errgroup для задач с ошибками
func withErrGroup(ctx context.Context) (int, int, error) {
var a, b int
var errA, errB error
var wg sync.WaitGroup
wg.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 - если задачи долгие, нет способа их отменить.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью