Я до сих пор помню свой первый запуск горутины. Только что перешёл с Java, где создание потока ощущалось как заполнение налоговой декларации: куча настроек, ExecutorService, пулы потоков и вечный страх, что всё рухнет в самый неподходящий момент.
А тут я просто написал go перед вызовом функции.
И всё. Никаких конфигураций. Никаких 15 строк подготовительного кода. Просто:
Но прежде чем углубляться в горутины, давайте разберёмся с базовыми понятиями, которые многие туториалы пролетают слишком быстро: последовательное, конкурентное и параллельное выполнение. Без этого фундамента дальше будет сложно.

Последовательное выполнение

Представьте кофейню с одним бариста. Приходит клиент, делает заказ, бариста полностью готовит напиток, отдаёт его и только потом зовёт следующего.
Это последовательное выполнение. Один процессор, одна задача за раз — от начала до конца.
Три заказа — шесть секунд. Каждый следующий ждёт, пока закончится предыдущий.

Конкурентное выполнение

Тот же один бариста, но очень ловкий. Он запускает кофемашину для эспрессо, а пока она работает — параллельно взбивает молоко для латте. Он не делает два дела одновременно в буквальном смысле, а быстро переключается между ними.
Это и есть конкурентность. Один процессор, несколько задач в работе одновременно. Ключевое слово — в работе, а не "одновременно".
Все три заказа стартуют почти мгновенно. Общее время около двух секунд. Тот же один процессор, но пропускная способность выросла в разы.

Параллельное выполнение

Теперь в кофейне три бариста. Каждый делает свой заказ одновременно, без всякого переключения.
Это параллелизм. Несколько ядер, каждая задача выполняется на своём ядре независимо.
Важный момент, который часто путают:
Конкурентность — это про структуру кода.
Параллелизм — это про исполнение.
Вы можете писать конкурентный код, который будет работать даже на одном ядре. А параллелизм без нескольких ядер невозможен.
Go прекрасно это разделяет. Вы пишете конкурентный код с помощью горутин, а рантайм сам решает, запускать ли их параллельно в зависимости от количества доступных ядер.

Что такое горутина

Горутина — это функция, которая выполняется конкурентно с другими функциями. Всё. Никаких специальных классов, интерфейсов или наследования. Просто ключевое слово go.
За кулисами горутина — это не OS-поток. Она намного легче. Обычному потоку нужно около 1 МБ стека, горутине — всего 2 КБ в начале (и стек растёт по мере необходимости). Можно запустить десятки и сотни тысяч горутин без особых проблем.

Анонимные горутины

Иногда не хочется создавать отдельную функцию. Можно запустить код конкурентно прямо на месте:
Синтаксис поначалу выглядит странно (определение функции + сразу вызов ()), но к нему быстро привыкаешь. В реальном Go-коде анонимные горутины встречаются повсеместно.

Планировщик Go

Когда запускается Go-программа, рантайм создаёт несколько OS-потоков и распределяет по ним горутины (M:N-планирование). Это делается через три основных сущности:
  • G — горутина
  • M — OS-поток (компьютер)
  • P — логический процессор (контекст планирования)
У каждого P есть своя локальная очередь горутин (Local Run Queue - LRQ) и есть глобальная очередь (Global Run Queue - GRQ).
Планировщик сам решает, когда и какую горутину запустить. Если горутина блокируется (например, на сетевом запросе), её просто вытесняют и ставят другую — вы этого даже не замечаете.

Кооперативное планирование

Планировщик Go кооперативный (не вытесняющий). Горутины сами отдают управление в определённых точках:
  • Вызов любой функции
  • Операции с каналами
  • Сетевой ввод/вывод
  • Сборка мусора
  • Запуск новой горутины (go)
Поэтому теоретически можно написать горутину в бесконечном цикле без единого вызова функции, которая зажмёт ядро. На практике такое почти не встречается, потому что почти всегда есть либо I/O, либо работа с каналами.

Главная ловушка новичков

Самая частая ошибка: когда завершается main(), программа сразу завершается, убивая все остальные горутины.
Поэтому в примерах мы использовали time.Sleep — это удобно для демонстрации, но в реальном коде так делать нельзя. Правильные решения — sync.WaitGroup и каналы.

Краткий итог

  • Последовательное — просто, но медленно.
  • Конкурентное — несколько задач в работе, быстрое переключение.
  • Параллельное — настоящее одновременное выполнение на разных ядрах.
  • Горутины — лёгкие, управляемые рантаймом Go, невероятно дешёвые.
  • Планировщик использует M:N-модель и локальные очереди.
  • Главная горутина завершилась, значит всё умерло.
Горутины — это не просто инструмент для ускорения. Это другой способ мышления о коде. После них многие привычные подходы кажутся тяжёлыми и архаичными.