Параллельное программирование может казаться сложным: нужно понимать, как работают потоки, блокировки, синхронизация и гонки данных. В 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 — тип данных, передаваемых через канал

На картинке видим, что небуферизированный канал блокирует отправителя до тех пор, пока получатель не прочитает данные.
Буферизированный канал имеет фиксированный размер буфера, в котором временно можем хранить отправленные данные:
ch := make(chan Type, n)
Где:
- Type — тип данных, передаваемых через канал;
- n — размер буфера (емость канала).

В буферизированном канале отправка и получение выполняются без блокировки, пока буфер позволяет это делать. Горутина-отправитель блокируется только при попытке записать данные в полностью заполненный буфер. А горутина-получатель блокируется только при попытке прочитать данные из пустого буфера.
Когда горутина ожидает получения данных из канала, а отправка больше не производится, возникает риск дедлока (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 помогают писать более производительный код и эффективно использовать ресурсы. Такой инструмент делает параллельное программирование доступным даже без глубокого погружения в низкоуровневые детали работы операционной системы.