Горутины (goroutine) в Go: что такое и зачем нужны

Горутины в Go: что это такое и зачем нужны

Мария Толстых
Мария Толстых Бэкенд-разработчик Go
22 июня 2026

В этой статье рассмотрим, как устроены горутины, чем они отличаются от процессов и потоков, а также как применять их на практике.

Изображение записи

Параллельное программирование может казаться сложным: нужно понимать, как работают потоки, блокировки, синхронизация и гонки данных. В Go для этого используется простой подход — горутины. Они позволяют выполнять несколько задач одновременно, писать производительный код и эффективно использовать ресурсы системы.

Что такое горутины (goroutines), чем они отличаются от процессов и потоков

Чтобы понять, как работают горутины, нужно сначала разобраться с более фундаментальными понятиями, такими как процессы и потоки. Именно на их фоне становится понятно, что делает горутины удобным и эффективным инструментом. 

Процесс — экземпляр программы, который выполняется в операционной системе. Или, другими словами, это единица активности операционной системы, в которой существуют последовательные действия, текущее состояние и набор связанных ресурсов.

У процесса свое адресное пространство: код, данные, стек, куча. А взаимодействовать процессы друг с другом могут через механизмы межпроцессорного взаимодействия.

Поток (Thread) — единица диспетчеризации и выполнения операционной системы. Он разделяет адресное пространство с процессом. То есть это небольшой процесс внутри процесса, который позволяет параллельно выполнять несколько задач в пределах одного процесса. 

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

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

Остановимся на асинхронном выполнении. Как мы знаем, существует синхронное (конкурентное) и асинхронное (параллельное) выполнение. 

Конкурентность дает возможность эффективно использовать ресурсы компьютера — так как мы можем переключаться между задачами в рамках одного ядра или процессора. А параллелизм позволяет одновременно выполнять несколько задач на разных ядрах или процессорах. Также мы можем совмещать эти два подхода. 

Разобрали уровень определений, теперь можем перейти к тому, из-за чего мы здесь

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

Горутины (goroutines) — легковесные потоки выполнения, которые позволяют параллельно выполнять код внутри программы. Это и есть реализация корутин в языке Go. Корутина — способ организации выполнения, поэтому используется как общее понятие.  

Горутина выполняются не операционной системой, а рантаймом самого Go. Она планируется специальным планировщиком Go. Планировщик оркестрирует горутинами по физическим потокам процессора для эффективной многозадачности. 

Пример: func main() {} — главная горутина

Зачем нужны горутины в Golang

Горутины заменяют сложные механизмы потоков операционной системы. С их помощью разработчик может запустить тысячи одновременно выполняемых функций, не опасаясь исчерпать ресурсы системы. Это позволяет эффективно решать задачи ввода-вывода (сетевые запросы, чтение файлов) и реализовывать асинхронное выполнение кода без сложных колбэков и явного управления пулом потоков.

Преимущества горутин

  • Горутины легковесны и занимают меньше ресурсов, чем потоки операционной системы. Множество горутин могут быть в одном процессе без особой угрозы производительности.
  • Встроенные примитивы (каналы chan) вместо сложных мьютексов и семафоров.
  • Планировщик Go автоматически распределяет горутины между доступными ядрами процессора (параллелизм), чередуя их выполнение с конкурентностью.

Как создать горутину в Go

Горутины создаются с использованием ключевого слова go перед

вызовом функции. Например:


      go func() {
// код будет выполнен в горутине
}()

func main() {} — главная горутина, которая не будет ждать завершения других горутин.

Горутины и каналы

Для обмена данными между горутинами существуют каналы (channel). Они обеспечивают безопасную синхронизацию, коммуникацию и обмен данными между горутинами. 

Канал включает несколько компонентов:

  • буфер — хранит определенное количество элементов в канале;
  • указатели на очереди — содержат горутины, ожидающие записи в канал или чтения из него;
  • указатели на тип данных — используются для контроля передаваемых значений;
  • мьютекс — защищает внутренние структуры канала от одновременного доступа нескольких горутин.

Канал можно создать с помощью функции make() и интерфейса от ключевого слова chan. Функция make() принимает тип канала и возвращает инициализированный канал, который готов к использованию.  


      ch := make(chan Type)

Каналы могут не иметь длины или иметь динамическую длину. Таким образом они делятся на небуферизированные и буферизированные.

Небуферизированный канал имеет синхронные отправку и получение данных. Он гарантирует, что данные будут переданы в правильном порядке.

Как создать небуферизированный канал:


      ch := make(chan Type)

Где Type — тип данных, передаваемых через канал

Две горутины (Goroutine 1 и Goroutine 2) соединены каналом (Channel). Goroutine 1 отправляет данные в канал — стрелка подписана «writes data». Данные проходят по каналу слева направо. Goroutine 2 получает данные из канала — стрелка подписана «reads data»
Визуализация работы небуферизированных каналов.

На картинке видим, что небуферизированный канал блокирует отправителя до тех пор, пока получатель не прочитает данные.

Буферизированный канал имеет фиксированный размер буфера, в котором временно можем хранить отправленные данные:


      ch := make(chan Type, n)

Где:

  • Type — тип данных, передаваемых через канал;
  • n — размер буфера (емость канала).
Диаграмма буферизованного канала в Go. Вверху — код создания канала:
ch := make(chan int, 5). Слева блок «A goroutine» с пунктирной стрелкой,
указывающей на канал — горутина записывает данные. Канал (Channel)
изображен в виде горизонтального прямоугольника, разделённого на 5 ячеек
(ёмкость буфера = 5). Из 5 ячеек 2 заняты — обозначены синими кругами,
3 ячейки пусты. Справа блок «Another goroutine» — получатель данных,
стрелка от канала к нему не показана.
Визуализация работы буферизированных каналов.

В буферизированном канале отправка и получение выполняются без блокировки, пока буфер позволяет это делать. Горутина-отправитель блокируется только при попытке записать данные в полностью заполненный буфер. А горутина-получатель блокируется только при попытке прочитать данные из пустого буфера.

Когда горутина ожидает получения данных из канала, а отправка больше не производится, возникает риск дедлока (deadlock). Deadlock в Go — ситуация, когда несколько горутин блокируются навечно в состоянии ожидания друг друга, чтобы освободить ресурсы.

Чтобы избежать deadlock, можно проверить, закрыт или открыт канал:


      ch := make(chan int, 7)
close(ch)
_, ok := <-ch 

Где ok — это bool-поле. Если true — канал открыт, если false — закрыт.

Горутины и пакет time

Порой разработчикам нужно управлять временем выполнения: делать задержки или ожидать завершения задач. Для этого в Go используются примитивы синхронизации и встроенный пакет time.

Самая простая функция — time.Sleep(). Она приостанавливает выполнение текущей горутины на заданный промежуток времени. Можно использовать в главной функции (func main() {}), чтобы дождаться выполнения внутренней горутины до завершения программы.


      package main

import (
	"fmt"
	"time"
)

func main() {
	k := 10 // переменная

	for i := 1; i < 5; i++ {
		// вызов горутины
		go func(n int) {
			fmt.Println(n*k) // вычисление
		}(i)
	}

	time.Sleep(200 * time.Millisecond)
}


В примере мы используем приостановку, чтобы успеть получить в выводе четыре строчки результатов вычислений.

Пакет time также предоставляет:

  • time.Now() — получение текущего времени;
  • time.After() — выполнение действия через заданное время;
  • time.Tick() — периодическое выполнение через интервалы;
  • time.NewTimer() и time.NewTicker() — более гибкое управление таймерами.

Важно понимать, что time.Sleep() не самое надежное решение. Для ожидания завершения горутин обычно используют примитивы синхронизации, такие как sync.WaitGroup. Они обеспечивают более надежную синхронизацию.

Горутины и многоядерные процессоры

В Go используется специальный планировщик, который самостоятельно под капотом распределяет горутины между системными потоками и ядрами процессора. Благодаря этому можно эффективнее использовать ресурсы многоядерных процессоров.

Если запустить большое количество горутин на одноядерной системе, они будут выполняться конкурентно. А на многоядерной системе часть из них сможет выполняться действительно параллельно.

Количество ядер, которые Go может использовать одновременно, регулируется параметром GOMAXPROCS. По умолчанию его значение равно количеству доступных ядер в системе, поэтому Go автоматически используют весь потенциал процессора.

Заключение

Горутины в Go помогают писать более производительный код и эффективно использовать ресурсы. Такой инструмент делает параллельное программирование доступным даже без глубокого погружения в низкоуровневые детали работы операционной системы.