Горутины и каналы
Конкурентность встроена в Go на уровне языка: запустить параллельную задачу можно одним ключевым словом go, а обмениваться данными между такими задачами — через каналы, типизированные «трубы» со встроенной синхронизацией. Это вводная тема: её цель — запустить горутину, передать в неё и из неё значения и корректно дождаться завершения, не углубляясь в устройство планировщика.
За кажущейся простотой стоят несколько правил, которые и проверяют на junior-секции. Горутина — это лёгкий поток, которым управляет рантайм Go, а не операционная система; когда main возвращается, программа завершается и все оставшиеся горутины обрываются. Отправка ch <- v в небуферизированный канал блокирует горутину, пока другая сторона не сделает приём <-ch — так канал заодно синхронизирует. Чтобы дождаться группы горутин, используют sync.WaitGroup. А когда писатель закончил, он вызывает close(ch), и читатель спокойно перебирает канал через for v := range ch. Тему разбираем по слоям — от запуска до закрытия канала. Устройство планировщика, select и паттерны конкурентности сюда не входят — они в темах Конкурентность и Каналы.
Карта темы
- Ключевое слово
go—go f()запускает горутину — лёгкий поток под управлением рантайма; она работает конкурентно, а возврат изmainобрывает все горутины разом. - Отправка и приём — канал — это типизированная «труба»
make(chan T);ch <- vотправляет,v := <-chпринимает; отправка в небуферизированный канал блокирует, пока кто-то не примет (базовая синхронизация). - Буфер и без буфера —
make(chan T, n)задаёт ёмкостьn; небуферизированный канал (ёмкость 0) синхронизирует каждую отправку с приёмом, буферизированная отправка блокирует только когда буфер полон. sync.WaitGroup—Add(n)до запуска,Done()(обычно черезdefer) в каждой горутине,Wait()блокирует до обнуления счётчика — канонический способ дождаться группы горутин.rangeиclose—close(ch)сигналит «значений больше не будет»;for v := range chвычитывает канал до закрытия; приём из закрытого канала даёт нулевое значение иok=false; закрывает только отправитель.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Запустить go f() и сразу вернуться из main | Программа завершится, горутина не успеет отработать — вывода не будет |
| Отправить в небуферизированный канал, когда никто не принимает | Горутина блокируется навсегда; если это main — deadlock и аварийный выход |
Считать make(chan T) и make(chan T, 1) одинаковыми | Без буфера отправка ждёт приёмника; с буфером на 1 одна отправка проходит сразу |
Вызвать wg.Add(1) внутри уже запущенной горутины | Wait() может увидеть нулевой счётчик и выйти раньше — гонка; Add делают до go |
Забыть wg.Done() в одной из горутин | Счётчик не дойдёт до нуля, Wait() зависнет навсегда |
| Закрыть канал на стороне читателя | Паника при отправке в закрытый канал; закрывать должен только отправитель |
Принимать из закрытого канала и не проверять ok | Получите нулевое значение типа и примете его за настоящее |
Значение для собеседований
Горутины и каналы — почти обязательный блок junior-секции по Go: с них начинают, потому что это и есть «фишка» языка. Глубокого знания планировщика на этом уровне не ждут, но базовую механику запуска и синхронизации спрашивают всегда.
Что обычно проверяют:
- Что делает
go f()и почему горутина «лёгкая» — ей управляет рантайм Go, а не ОС. - Почему программа из
go fmt.Println(...)вmainчасто не печатает ничего (возврат изmainобрывает горутины). - Чем отправка в небуферизированный канал отличается от буферизированного и что значит «канал блокирует».
- Как дождаться нескольких горутин через
sync.WaitGroupи почемуAddставят доgo. - Кто и зачем вызывает
close, что отдаёт приём из закрытого канала и какfor rangeпо каналу завершается.