Что такое горутины
Горутины (goroutines) — это легковесные потоки исполнения в языке Go, предназначенные для параллельной и конкурентной обработки. Они позволяют запускать функции асинхронно и эффективно использовать ресурсы многопроцессорных систем, обеспечивая масштабируемую многозадачность с минимальными накладными расходами.
Горутины реализуют модель конкурентности, основанную на идеях CSP (Communicating Sequential Processes), где потоки исполнения обмениваются данными через каналы и синхронизируются безопасно и просто.
1. Основы горутин
Чтобы создать горутину, достаточно использовать ключевое слово go перед вызовом функции:
go myFunction()
Это запускает myFunction параллельно с остальной программой. Вызов не блокирует основное выполнение и выполняется "в фоне".
Пример:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from goroutine")
}
func main() {
go sayHello()
time.Sleep(time.Second) // Даем горутине время завершиться
}
2. Планировщик горутин
Горутины работают под управлением встроенного планировщика Go, который мультиплексирует тысячи (и более) горутин поверх небольшого количества системных потоков (обычно равного количеству доступных CPU, но можно настроить через GOMAXPROCS).
В отличие от потоков ОС, горутины:
-
создаются в разы быстрее;
-
требуют меньше памяти (обычно от 2 кБ стека);
-
переключаются быстрее, без вмешательства ядра.
Планировщик Go реализует кооперативную многозадачность: горутины добровольно передают управление другим горутинам во время определённых точек ("preemption points") — вызовов каналов, select, time.Sleep, runtime.Gosched() и т.п.
3. Стек и управление памятью
Каждая горутина начинается с небольшого стека (обычно 2 кБ), который динамически расширяется по мере необходимости до мегабайт. Это позволяет запускать тысячи и даже миллионы горутин без исчерпания памяти.
В отличие от потоков ОС, где стек фиксирован, стек горутин управляется сборщиком мусора и растёт/сжимается автоматически.
4. Передача данных между горутинами
Горутины обычно взаимодействуют через каналы, что делает их синхронизированными и потокобезопасными:
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j \* 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
go worker(jobs, results)
jobs <- 1
jobs <- 2
close(jobs)
fmt.Println(<-results) // 2
fmt.Println(<-results) // 4
}
Такой способ минимизирует риск гонок и необходимости использовать мьютексы.
5. Горутины и анонимные функции
Можно использовать анонимные функции для запуска горутин:
go func(msg string) {
fmt.Println(msg)
}("Hello")
Анонимные горутины часто используются в циклах, при условии правильной обработки замыканий (closures), чтобы избежать неожиданных значений.
6. Ожидание завершения горутин
Поскольку go не блокирует выполнение, основная программа может завершиться, не дождавшись завершения фоновых горутин. Для контроля используют:
sync.WaitGroup
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// работа горутины
}()
wg.Wait() // блокируем до завершения всех горутин
Каналы:
done := make(chan bool)
go func() {
// работа
done <- true
}()
<-done // блокировка, пока горутина не отправит сигнал
7. Параллелизм и конкурентность
-
Concurrency (конкурентность) — выполнение нескольких задач одновременно, не обязательно в параллель (могут поочередно использовать один поток).
-
Parallelism (параллелизм) — выполнение задач одновременно на разных ядрах процессора.
Go позволяет и то, и другое. Параллелизм настраивается через:
runtime.GOMAXPROCS(n) // число доступных OS-потоков
По умолчанию равно количеству логических процессоров.
8. Ошибки и утечки горутин
Неправильное использование горутин может привести к:
-
утечкам: горутина блокируется и никогда не завершается (например, ждет из канала, который никто не пишет);
-
панике: если горутина вызывает panic, её нужно перехватывать через recover внутри самой горутины;
-
гонкам данных: если несколько горутин обращаются к одной переменной без синхронизации (решается через sync.Mutex, каналы или atomic).
Пример утечки:
func leaky() {
ch := make(chan int)
go func() {
ch <- 1 // блокируется навсегда, если никто не читает
}()
}
9. Горутины в цикле и замыкания
Одна из частых ошибок — горутина в цикле, использующая переменные цикла без копии:
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // все 3 горутины могут напечатать "3"
}()
}
Правильно:
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println(i) // напечатает 0, 1, 2
}(i)
}
10. Запуск миллионов горутин
Благодаря лёгкости и малым накладным расходам можно запускать сотни тысяч и миллионы горутин:
for i := 0; i < 1_000_000; i++ {
go func() {
time.Sleep(time.Second)
}()
}
В системах с многопоточностью такого бы не получилось без проблем с памятью или производительностью.
11. Примеры практического применения
-
HTTP-сервер: каждая обработка запроса в своей горутине.
-
Параллельная обработка файлов.
-
Обработка задач из очереди (например, jobs/workers).
-
Скачивание данных из сети параллельно.
-
Таймеры и отложенные задачи (через time.After, time.Timer).
12. Сравнение с потоками ОС
Параметр | Горутины | Потоки ОС |
---|---|---|
Время запуска | Миллисекунды | Микросекунды |
--- | --- | --- |
Стартовый стек | ~2 КБ | ~1 МБ |
--- | --- | --- |
Масштабируемость | 100 тыс и больше | Обычно до 10 тыс |
--- | --- | --- |
Управление | Go runtime | ОС (ядро) |
--- | --- | --- |
Переключение | Кооперативное | Прерывание ядром |
--- | --- | --- |
Контекст-переключение | Быстрое | Дорогостоящее |
--- | --- | --- |
13. Инструменты для отладки и анализа горутин
-
runtime.NumGoroutine() — возвращает количество активных горутин.
-
pprof — профилирование горутин.
-
trace — трассировка выполнения горутин и событий.
-
go test -race — проверка на гонки данных.
14. Примеры в стандартной библиотеке Go
Многие функции и пакеты Go используют горутины под капотом:
-
http.ListenAndServe — каждая сессия обрабатывается в отдельной горутине.
-
time.Ticker, time.Timer — используют горутины.
-
context — передача сигналов отмены между горутинами.