Go concurrency distilled: шпаргалка по горутинам и каналам

27.09.2026 · 5 мин

Если вы путаетесь в горутинах, каналах и мьютексах, Go concurrency может казаться хаосом. На деле это набор простых примитивов, из которых собирается удобная и предсказуемая система.

Почему GO подходит для конкурентности

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

Горутины: ваши рабочие

Горутина — это функция, запущенная одновременно с другими. Синтаксис простой: go функция(). Рантайм Go сам распределяет горутины по потокам операционной системы, поэтому вам не нужно вручную управлять этим соответствием.

ГОРУТИНЫ И ПОТОКИ
─────────────────
┌─────────────────────────────────────────────────────────┐
│                    OS Thread Pool                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐│
│  │ Thread 1 │  │ Thread 2 │  │ Thread 3 │  │ Thread 4 ││
│  ├───┬───┤  ├───┬───┤  ├───┬───┤  ├───┬───┤  │
│  │ g1 │ g2│  │ g3 │   │  │ g4 │g5 │  │   │   ││
│  └───┴───┘  └───┴───┘  └───┴───┘  └───┴───┘│
└─────────────────────────────────────────────────────────┘

g1-g5 = горутины, распределённые по потокам
Рантайм Go сам решает, какую горутину выполнить и на каком потоке

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

Waitgroup: ожидание без магии

WaitGroup — это счётчик, который помогает дождаться завершения группы горутин. У него есть три основных метода: Add(n), Done() и Wait(). Иногда ещё используют wg.Go(), чтобы запуск и учёт горутины происходили в одном вызове.

WAITGROUP ЖИЗНЕННЫЙ ЦИКЛ
────────────────────────
main: Add(2) ──▶ запуск go worker1()
          └──▶ запуск go worker2()

worker1: [выполнение...] Done()
worker2: [выполнение...] Done()

main: Wait() ──▶ счётчик=0 ──▶ продолжаем
WaitGroup отслеживает, сколько горутин ещё работают

Каналы: общение между горутинами

Канал — это способ передавать данные между горутинами. Создаётся он так: ch := make(chan string). По умолчанию канал небуферизированный: отправитель блокируется, пока получатель не заберёт значение. Это создаёт синхронную передачу данных.

Буферизированные каналы

Если указать размер буфера, канал начинает работать как очередь FIFO с ограниченной ёмкостью: ch := make(chan int, 3). Пока буфер не заполнен, запись не блокирует; пока буфер не пуст, чтение тоже не блокирует. Это удобно для сглаживания нагрузки.

Закрытие канала

Закрытие канала означает, что новых данных больше не будет. Получатель может узнать об этом через идиому comma ok: val, ok := <-ch. Если ok == false, канал закрыт. Часто для чтения до конца используют range по каналу.

Направленные и nil-каналы

Go позволяет ограничивать направление канала в сигнатурах функций: chan T — читать и писать, chan<- T — только писать, <-chan T — только читать. Это помогает ловить ошибки на этапе компиляции. Отдельно стоит помнить, что nil-канал блокирует навсегда при любой операции, и этим иногда пользуются для отключения ветки в select.

Select: мультиплексирование каналов

select работает с несколькими каналами сразу: выбирает готовый case, а если готовых несколько — делает случайный выбор. Если ни один не готов и есть default, выполняется он; если default нет — ожидание продолжается. Это основа таймаутов, отмены и других паттернов управления потоками данных.

Синхронизация: мьютексы, атомики и прочее

Каналы — не единственный способ синхронизации. Для простых случаев есть примитивы из sync: Mutex для защиты разделяемого состояния, RWMutex для сценариев с множеством читателей, WaitGroup для ожидания завершения, Once для одноразовой инициализации, Pool для переиспользования объектов и atomics для простых счётчиков и флагов.

Типичные ошибки

Состояние гонки возникает, когда две горутины одновременно читают и пишут одни и те же данные. Go умеет искать такие баги через -race.

Взаимная блокировка появляется, когда горутины навсегда ждут друг друга. Частая причина — неверная работа с каналами: например, забыли буфер или закрытие.

Утечка горутин — это когда горутина живёт, но никогда не завершается. Обычно так бывает, если одна из сторон канала навсегда заблокировалась.

Практический чеклист

Выводы

Concurrency в Go строится на коротком наборе идей: горутины, каналы, select и примитивы sync. Если понимать их роли, код становится проще и надёжнее. Если не понимать — появляются гонки, deadlock и утечки горутин.

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

Ссылки

Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.