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

Что нужно до начала
Горутина — это легковесный поток выполнения, который управляется рантаймом Go, а не операционной системой. Создать её стоит дешевле, чем системный поток: начальный стек занимает около 2–8 КБ (рантайм наращивает его по мере надобности). Именно поэтому в одном процессе спокойно живут десятки тысяч горутин там, где OS-потоки давно упёрлись бы в лимит памяти.
Чтобы повторить всё, что ниже, понадобится:
- Go 1.22 или новее — проверьте командой
go version; - любой текстовый редактор или IDE с поддержкой Go (VS Code + расширение Go, GoLand);
- терминал, в котором доступна команда
go run.
В конце у вас будет работающая программа с несколькими горутинами, каналом и корректным завершением. На всё уйдёт 20–30 минут.
Шаг 1. Запускаем горутину ключевым словом go
Создайте файл main.go в пустой директории и инициализируйте модуль:
mkdir goroutine-demo && cd goroutine-demo
go mod init goroutine-demo
Откройте main.go и вставьте:
package main
import (
"fmt"
"time"
)
func printMessage(msg string) {
for i := 0; i < 3; i++ {
fmt.Println(msg, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Запускаем функцию как горутину
go printMessage("горутина")
// Основная функция тоже работает
printMessage("main")
}
Запустите:
go run main.go
Проверка: в выводе строки горутина 0, main 0 и т.д. перемешаны — порядок не гарантирован. Если вы видите только строки main, а горутина не появилась — значит main завершился раньше, чем горутина успела напечатать. Об этом — следующий шаг.
Ключевое слово go перед вызовом функции — это всё, что нужно. Никаких классов, пулов или фабрик. Рантайм Go сам распределяет горутины по OS-потокам через свой планировщик (модель M:N).
Шаг 2. Ждём завершения через sync.WaitGroup
Функция main — тоже горутина. Когда она возвращается, процесс завершается, и все остальные горутины умирают без предупреждения. time.Sleep в качестве «ожидания» — антипаттерн: вы не знаете, сколько реально займёт работа. Правильный примитив — sync.WaitGroup.
package main
import (
"fmt"
"sync"
"time"
)
func printMessage(msg string, wg *sync.WaitGroup) {
// Сообщаем WaitGroup, что горутина завершилась
defer wg.Done()
for i := 0; i < 3; i++ {
fmt.Println(msg, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // ожидаем две горутины
go printMessage("первая", &wg)
go printMessage("вторая", &wg)
wg.Wait() // блокируемся, пока счётчик не станет 0
fmt.Println("все горутины завершились")
}
Запустите go run main.go.
Проверка: строка все горутины завершились всегда появляется последней. Если программа зависла — вы забыли вызвать wg.Done() в одной из горутин, и счётчик никогда не дойдёт до нуля.
Частая ловушка: передача WaitGroup по значению
sync.WaitGroup содержит внутреннее состояние. Если передать его по значению (без &), горутина работает с копией, Done уменьшает копию, а Wait в main висит вечно. Передавайте указатель.
Шаг 3. Передаём данные через канал
Горутины обмениваются данными через каналы, а не через разделяемую память. Это главная идиома Go:
Do not communicate by sharing memory; instead, share memory by communicating.
— Effective Go, официальная документация
Перепишем пример: каждая горутина считает сумму своего среза чисел и отправляет результат в канал.
package main
import "fmt"
func sumSlice(numbers []int, resultCh chan<- int) {
total := 0
for _, n := range numbers {
total += n
}
// Отправляем результат в канал
resultCh <- total
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Небуферизированный канал: отправка блокируется, пока кто-то не прочитает
resultCh := make(chan int)
mid := len(nums) / 2
go sumSlice(nums[:mid], resultCh)
go sumSlice(nums[mid:], resultCh)
// Читаем два результата — по одному от каждой горутины
part1 := <-resultCh
part2 := <-resultCh
fmt.Println("итого:", part1+part2) // 55
}
Проверка: вывод — итого: 55. Канал здесь играет роль и средства передачи данных, и механизма синхронизации: main блокируется на чтении, пока горутина не отправит значение.
Сноска: Небуферизированный канал — канал, созданный без указания размера буфера (make(chan int)). Операция отправки в такой канал блокирует горутину до тех пор, пока другая горутина не выполнит чтение из него, и наоборот. Буферизированный канал (make(chan int, 10)) позволяет отправить до N значений без блокировки.
Шаг 4. Проверяем гонку данных
Самая коварная ошибка новичка — обращение к одной переменной из нескольких горутин без синхронизации. Go имеет встроенный детектор гонок.
go run -race main.go
Если детектор нашёл гонку, он напечатает трассировку с адресами чтения и записи и завершит программу с ненулевым кодом. Привыкайте запускать тесты с флагом -race — это дешёвая страховка.
В примерах выше гонок нет: в шаге 2 каждая горутина печатает свою строку и не делит данные, в шаге 3 данные передаются через канал. Но стоит завести общий счётчик и инкрементировать его из двух горутин без мьютекса — детектор сработает.
Если не получилось
| Симптом | Вероятная причина | Что делать |
|---|---|---|
| Горутина не печатает ничего | main завершается раньше | Добавьте sync.WaitGroup или канал для ожидания |
| Программа зависла навсегда | Чтение из канала, в который никто не пишет, или wg.Done() не вызван | Проверьте, что количество Add совпадает с количеством Done; убедитесь, что в канал отправляется столько значений, сколько читается |
fatal error: all goroutines are asleep - deadlock! | Все горутины заблокированы на каналах или мьютексах | Рантайм Go обнаружил, что ни одна горутина не может продвинуться. Ищите несбалансированное чтение/запись в каналы |
DATA RACE при запуске с -race | Две горутины обращаются к одной переменной без синхронизации | Передавайте данные через канал или защищайте доступ sync.Mutex |
go: cannot find main module | Нет файла go.mod в директории | Выполните go mod init имя-модуля |
Что делать дальше
Горутина и канал — фундамент, но не вся картина. Следующие шаги:
- Разберитесь с
context.Context— он позволяет отменять группу горутин по таймауту или сигналу. Без него любой сетевой сервис на Go рискует копить зависшие горутины. - Попробуйте
select— оператор, который позволяет ждать сразу на нескольких каналах и реагировать на первый готовый. - Запустите тесты с флагом
-raceв CI — это бесплатный способ ловить гонки до продакшена. - Прочитайте раздел «Concurrency» в Effective Go (ссылка ниже) — там описаны паттерны, которые в экосистеме Go считаются каноническими.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: Effective Go — Concurrency (официальная документация Go), The Go Memory Model (официальная спецификация)
Частые вопросы
Чем горутина отличается от потока ОС?
Можно ли вернуть значение из горутины напрямую?
Сколько горутин можно запустить одновременно?
Что будет, если main завершится раньше запущенных горутин?
Как отладить, если программа зависла и я не понимаю, где?
Работает ли флаг -race в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.