> Как уведомить горутины в Go о необходимости завершения при получении первого результата (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Black Wall Group (BWG)

Стек: Go

> Пример ответа

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

Для уведомления горутин о завершении при получении первого результата используйте context.WithCancel в связке с sync.Once или канал закрытия. Основная горутина создаёт контекст, запускает воркеры, и первый успешный результат вызывает cancel(). Альтернатива - буферизованный канал результатов с select в главной горутине и закрытие общего канала done. Выбор зависит от того, нужно ли дожидаться завершения воркеров или можно завершить их принудительно через context.

Подробное объяснение

Задача - остановить конкурентные горутины после того, как одна из них вернула результат. Есть два базовых паттерна:

  1. context.WithCancel - самый распространённый. Создаётся контекст, воркеры слушают ctx.Done(). Первый воркер, получивший результат, вызывает cancel(). Остальные воркеры при следующей проверке контекста завершаются. Важно: cancel() - идемпотентна, её можно вызывать многократно, но для чистоты используют sync.Once.

  2. Канал done - закрытие канала сигнализирует всем горутинам. Закрытие канала - broadcast-операция, все читатели получают нулевое значение. Но закрывать канал можно только один раз, поэтому нужен sync.Once или отдельная горутина-контроллер.

Ключевой момент - не гонка за результат, а гарантия того, что все горутины увидят сигнал завершения. context делает это через внутренний канал done, который закрывается при вызове cancel().

Для сбора первого результата обычно используют буферизованный канал размером 1: первый воркер отправляет результат и вызывает cancel(), остальные блокируются на отправке или проверяют контекст.

Важно понимать: cancel() не убивает горутины принудительно. Она лишь даёт им сигнал. Горутина должна сама проверить ctx.Done() в удобной точке - например, в цикле или перед блокирующей операцией.

На практике

В реальном коде паттерн выглядит так:

  • Создаёте ctx, cancel := context.WithCancel(context.Background())
  • Запускаете N воркеров, каждый получает ctx и канал для результата
  • Воркер выполняет работу, проверяя ctx.Err() на каждой итерации
  • При получении результата воркер отправляет его в канал и вызывает cancel()
  • Главная горутина читает из канала результат и завершает работу

Для гарантии однократного вызова cancel() используйте sync.Once - это защита от случайного множественного вызова, хотя cancel() и так идемпотентна.

Если воркеры выполняют блокирующие операции (сетевые запросы, чтение из БД), нужно передавать ctx в эти операции - большинство библиотек Go поддерживают контекст.

Пример кода

GO
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, resultCh chan<- string, cancel context.CancelFunc, once *sync.Once) {
// Имитация работы с проверкой контекста
select {
case <-time.After(time.Duration(id) * 100 * time.Millisecond):
// Первый воркер (id=1) вернёт результат быстрее всех
once.Do(func() {
resultCh <- fmt.Sprintf("result from worker %d", id)
cancel()
})
case <-ctx.Done():
fmt.Printf("worker %d cancelled\n", id)
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resultCh := make(chan string, 1)
var once sync.Once
for i := 1; i <= 5; i++ {
go worker(ctx, i, resultCh, cancel, &once)
}
select {
case res := <-resultCh:
fmt.Println("Got:", res)
case <-time.After(2 * time.Second):
fmt.Println("timeout")
}
// Даём остальным горутинам завершиться
time.Sleep(200 * time.Millisecond)
}

Как отвечать на собеседовании

Начните с context.WithCancel как основного инструмента. Объясните, что cancel() - это сигнал, а не принудительное завершение. Упомяните sync.Once для защиты от множественного вызова. Затем покажите альтернативу с закрытием канала done и объясните, почему context удобнее - он встроен в стандартную библиотеку и поддерживается всеми сетевыми операциями.

Подчеркните важность проверки ctx.Done() в воркерах - без этого горутины не увидят сигнал. Если воркер выполняет блокирующий вызов, нужно передавать контекст в этот вызов.

Хорошо добавить про errgroup из golang.org/x/sync - он решает эту задачу из коробки, но уточните, что это внешняя библиотека.

Что проверяет интервьюер

  • Понимание модели конкурентности Go: горутины не убиваются принудительно, только сигнализируются
  • Знание context и его внутреннего устройства (канал done)
  • Умение работать с sync.Once для идемпотентных операций
  • Понимание разницы между context и простым закрытием канала
  • Способность избегать гонок данных при отправке результата

Типичные ошибки

  • Вызов cancel() без sync.Once - хотя это не ошибка, но показывает непонимание идемпотентности
  • Отправка результата в небуферизованный канал без select - воркер может заблокироваться навсегда, если главная горутина уже ушла
  • Игнорирование ctx.Done() в воркере - горутина продолжит работать после сигнала
  • Закрытие канала done несколько раз - паника
  • Использование time.Sleep для ожидания завершения воркеров вместо sync.WaitGroup или errgroup
  • Передача context.Background() вместо context.WithCancel - теряется возможность отмены

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

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