Можно ли сделать функцию с горутинами, которую можно вызывать и синхронно, и асинхронно в Go
Что проверяют
Знаете ли вы соглашение Go про асинхронность: конкурентность выбирает вызывающий, а не сама функция. Попутно проверяют понимание утечек горутин и того, как из горутины вообще возвращают результат.
Как отвечать
Технически да, но правильный ответ начинается с того, что так не делают. Функция должна быть синхронной, а тот, кто её зовёт, сам решает, писать перед вызовом go или нет. Флаг async параметром означает два разных контракта у одной функции, и читателю кода придётся каждый раз смотреть на аргумент.
Если асинхронный вариант правда нужен, его оформляют отдельной обёрткой, возвращающей канал:
func FetchAsync(ctx context.Context, id int) <-chan Result {
ch := make(chan Result, 1)
go func() { ch <- Fetch(ctx, id) }()
return ch
}Важные детали такой обёртки: буфер на единицу, чтобы горутина не зависла навсегда, если вызывающий передумал ждать; context для отмены; ошибка едет внутри результата, а не возвращается наружу. Для группы задач самописную обвязку заменяют на sync.WaitGroup или errgroup, второй ещё и соберёт первую ошибку и отменит остальных.
Где спотыкаются
Первое: передают канал внутрь функции и пишут в него из вызывающей горутины. Если канал небуферизованный и читателя в этот момент нет, встаёт вся программа: рантайм сообщает, что все горутины спят, и завершает процесс.
Второе: считают, что запуск горутины ускоряет сам по себе. Если тяжёлая работа выполняется до go, никакой асинхронности не появилось: вызывающий уже отстоял своё, а в отдельной горутине осталась только отправка готового значения.
Третье: забывают, что горутина живёт своей жизнью. Никто её не ждёт по умолчанию, main завершится и оборвёт работу на середине, а паника внутри горутины уронит весь процесс, снаружи её не перехватить. recover должен стоять в самой горутине.