Каналы: как горутины общаются
Разбираем вопрос о том, как горутины безопасно обмениваются данными друг с другом — без общих переменных и ручных блокировок.
Канал (channel) — это типизированная "труба" для передачи данных между горутинами. Одна горутина кладёт значение в канал, другая его забирает, и Go сам гарантирует, что доступ к данным будет безопасным.
Вопрос: зачем нужны каналы, если есть обычные переменные?
Чёткий ответ. Если несколько горутин одновременно читают и пишут одну и ту же обычную переменную, это гонка данных (data race) — непредсказуемое поведение, которое трудно отловить и воспроизвести. Каналы решают эту проблему на уровне языка: в философии Go это звучит как правило "не общайтесь через разделяемую память — делитесь памятью, общаясь через канал". Канал сам берёт на себя всю синхронизацию.
Создаётся канал функцией make, а тип элементов указывается через chan. Отправка — оператор <- после имени канала, приём — <- перед именем.
package main
import "fmt"
func main() {
ch := make(chan string) // небуферизованный канал строк
go func() {
ch <- "привет из горутины" // отправка в канал
}()
msg := <-ch // приём из канала (блокирует main до отправки)
fmt.Println(msg)
}
Вывод:
привет из горутины
Небуферизованные каналы: отправка и приём — это рандеву
Канал из примера выше — небуферизованный (объявлен без второго аргумента в make). Такой канал не хранит значения "про запас": отправляющая горутина блокируется на строке ch <- значение до тех пор, пока какая-то другая горутина не будет готова его тут же забрать. Это похоже на встречу двух людей: оба должны оказаться в условленной точке одновременно, иначе один из них ждёт.
Именно поэтому в примере выше main не завершается раньше времени, хотя мы не звали time.Sleep: строка msg := <-ch сама блокирует выполнение, пока горутина не пришлёт значение.
Буферизованные каналы: труба с "полочкой"
Если каналу передать вторым аргументом число, получится буферизованный канал — у него есть внутренняя очередь на заданное число элементов. Отправка блокируется, только когда буфер полностью заполнен; приём — только когда он полностью пуст.
package main
import "fmt"
func main() {
ch := make(chan int, 2) // буфер на 2 элемента
ch <- 1 // не блокирует: в буфере есть место
ch <- 2 // тоже не блокирует
fmt.Println("отправили 2 значения, не заблокировавшись")
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Вывод:
отправили 2 значения, не заблокировавшись
1
2
Если бы мы попытались отправить в этот канал третье значение без предварительного чтения — ch <- 3 заблокировала бы горутину, потому что буфер уже полон. Это частый вопрос на собеседовании: "будет ли этот код дедлоком?" — надо аккуратно посчитать, сколько значений умещается в буфер и кто их читает.
Закрытие канала и чтение "до дна"
Канал можно закрыть функцией close(ch) — это сигнал получателям "больше отправок не будет". После закрытия из канала всё ещё можно вычитать всё, что там осталось, а затем приём начинает возвращать нулевое значение. Чтобы отличить "пришло настоящее значение 0" от "канал закрыт и пуст", используют форму приёма с двумя значениями.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 10
ch <- 20
close(ch) // отправок больше не будет
for {
v, ok := <-ch
if !ok {
fmt.Println("канал закрыт и пуст")
break
}
fmt.Println("получили:", v)
}
}
Вывод:
получили: 10
получили: 20
канал закрыт и пуст
Такой же перебор гораздо чаще пишут через range — он сам останавливается, когда канал закрыт и пуст, без ручной проверки ok:
for v := range ch {
fmt.Println("получили:", v)
}
Частые ошибки на собеседовании
- Путают буферизованный и небуферизованный канал — думают, что
make(chan int)тоже что-то буферизует. - Забывают закрыть канал там, где его читают через
range— тогда цикл зависает навечно (дедлок). - Пытаются отправить значение в уже закрытый канал — это паника времени выполнения (
panic: send on closed channel). - Не понимают, что закрывать канал должен именно отправитель, а не получатель — иначе легко словить гонку или двойное закрытие.
Итог
- Канал — типобезопасная труба для обмена данными между горутинами, синхронизация встроена.
- Небуферизованный канал — рандеву: отправка ждёт готового получателя.
- Буферизованный канал блокирует только при заполненном (отправка) или пустом (приём) буфере.
- Форма
v, ok := <-chи циклrangeпозволяют корректно определить, что канал закрыт.