Гарантирует ли оператор go немедленное начало выполнения новой горутины до перехода к следующей инструкции?
Нет, оператор go не гарантирует немедленное начало выполнения горутины. Он только планирует вызов функции для конкурентного выполнения, после чего текущая горутина продолжает работу независимо от того, успела ли новая горутина стартовать.
Если порядок выполнения важен, его нужно явно обеспечить каналом, sync.WaitGroup, sync.Cond, мьютексом или другим примитивом синхронизации. Сам факт запуска через go не создаёт такой гарантии.
Горутины были введены как лёгкая модель конкурентного выполнения поверх потоков, управляемых рантаймом Go. Такой подход позволяет запускать большое количество задач, не создавая отдельный поток операционной системы для каждой из них.
Для этого рантайм самостоятельно планирует горутины на доступных потоках. Разделение между запуском горутины и её фактическим выполнением позволяет эффективнее использовать процессор, но одновременно означает, что программист не должен полагаться на конкретный порядок их запуска.
После оператора go текущая функция может немедленно выполнить следующую инструкцию, завершиться или заблокироваться. Новая горутина может начать выполнение до этого, позже или не начать его вообще, если процесс завершится раньше.
Зависимость от предполагаемого порядка приводит к гонкам, пропущенным результатам и нестабильным тестам. Особенно опасны попытки использовать sleep как способ «дать горутине время стартовать»: задержка не создаёт формальной гарантии и зависит от планировщика и нагрузки.
Оператор go вычисляет аргументы вызова в текущей горутине, а сам вызов выполняется конкурентно в новой горутине. После постановки работы в планировщик текущая горутина не ждёт ни начала выполнения, ни завершения вызова.
В этом примере сообщение может быть напечатано, а может не быть напечатано: main способна завершиться раньше новой горутины. Это не означает, что оператор go не сработал; завершение main завершает процесс и не оставляет времени на выполнение остальных горутин.
Для ожидания завершения используют sync.WaitGroup, а для передачи сигнала о готовности или результате — каналы. Выбор зависит от задачи: WaitGroup сообщает о завершении группы работ, канал передаёт значения или события и может выразить более содержательный протокол взаимодействия.
Планировщик Go не предоставляет прикладному коду гарантий вроде «новая горутина начнёт выполняться до следующей строки». Даже наличие нескольких логических процессоров не превращает порядок запуска в детерминированный. Увеличение числа процессоров может изменить наблюдаемое поведение, но не должно использоваться для исправления синхронизации.
Сервис запускает фоновую горутину для отправки метрик и сразу возвращает управление вызывающему коду. В тесте разработчик добавляет короткую задержку, после чего проверяет, что метрика отправлена. Тест периодически падает на медленной машине или в условиях высокой нагрузки.
Рассматривались два варианта. Увеличение задержки просто маскирует проблему и замедляет тесты, а ожидание фиксированного числа миллисекунд всё равно не гарантирует завершение работы. Передача канала завершения делает состояние явным, но требует определить, кто отвечает за закрытие или отправку сигнала.
Выбран вариант с каналом подтверждения: фоновая горутина отправляет сигнал после успешной отправки метрики, а тест ждёт этот сигнал с ограничением по тайм-ауту. В результате ожидание стало детерминированным, а тайм-аут сохранил защиту от зависания при ошибке внешней системы.
Может ли новая горутина начать выполняться до того, как оператор go завершит вычисление аргументов?
Нет. Аргументы вызова вычисляются в текущей горутине до передачи вызова на выполнение новой. Это гарантирует момент вычисления значений, но не гарантирует момент начала самого вызова и не синхронизирует последующие изменения объектов, на которые указывают аргументы.
Делает ли запуск через go доступ к общим данным безопасным?
Нет. Горутины могут одновременно читать и изменять одну переменную, поэтому без синхронизации возникает гонка данных. Для безопасного взаимодействия применяют каналы, мьютексы, атомарные операции или другие средства; конкурентный запуск сам по себе не устанавливает необходимого порядка памяти.
Почему runtime.Gosched не является заменой синхронизации?
runtime.Gosched лишь позволяет текущей горутине уступить процессор, после чего она может снова получить выполнение раньше ожидаемой горутины. Функция не ждёт наступления события и не создаёт отношения happens-before. Для гарантии используют операцию, которая действительно связывает действия горутин: например, отправку и получение через канал или ожидание WaitGroup.