Как правильно организовать обработку задачи, вызываемой из нескольких мест в Go?
Что проверяют
Понимаете ли вы, что общая функция, вызываемая из разных мест, нуждается в защите общего состояния и в управлении жизненным циклом.
Как отвечать
Сначала уточните смысл: задача выполняется по требованию из разных мест, и её нужно либо просто сделать потокобезопасной, либо не выполнять параллельно дважды.
Типовые решения:
- чистая функция без общего состояния: ничего защищать не нужно, вызывайте откуда угодно. Это идеальный случай, к нему и стоит стремиться;
- общее состояние: защищается мьютексом или прячется за одной горутиной-владельцем, которая принимает запросы через канал. Второй вариант в духе Go: состояние принадлежит одной горутине, остальные шлют ей сообщения;
- выполнять ровно один раз:
sync.Onceдля инициализации,singleflight.Groupдля схлопывания одинаковых одновременных запросов, чтобы десять вызовов не породили десять походов в базу; - очередь задач: если работа тяжёлая, вызовы кладут задание в канал, а пул воркеров разбирает. Так число одновременных выполнений ограничено, а вызывающие не блокируются;
- периодическая задача: отдельная горутина с тикером, а вызовы извне только просят её выполниться внеочередно через канал.
Обязательно назовите общие правила: первым параметром идёт контекст, чтобы вызов можно было отменить и ограничить по времени; у горутины должен быть понятный владелец и способ завершиться; результат возвращается вызывающему явно, а не через общую переменную; гонки проверяются флагом обнаружения гонок в тестах.
Где спотыкаются
Прячут общее состояние в пакетную переменную без синхронизации и получают гонку, которая ловится раз в неделю.
Запускают горутину на каждый вызов без ограничения и заваливают внешний сервис.
Не передают контекст, из-за чего отменить работу после ухода клиента невозможно.