Перейти к содержимому
Теория · Горутины и каналы · урок 4 из 13

Каналы

Горутина может посчитать значение, но оператор go отбрасывает результат функции. Как повару передать готовое блюдо официанту? На кухне для этого есть окно раздачи: повар ставит тарелку с одной стороны, официант забирает её с другой.

Значение из горутины

В Go такое окно — канал. Одна горутина отправляет в него значения, а другая их принимает:

GO
package main

import "fmt"

func main() {
    ch := make(chan int)
    go func() {
        ch <- 6 * 7
    }()
    fmt.Println(<-ch) // 42
}

Давайте разберём пример по строкам:

  1. ch := make(chan int) прорубает окно для значений int.
  2. go func() { … }() запускает горутину-повара: она считает 6 * 7 и отправляет 42 в канал.
  3. fmt.Println(<-ch) — main в роли официанта ждёт у окна, забирает 42 и печатает его.

Тип канала

Канал создают через make: make(chan int) даёт канал для значений int, и его тип — chan int. Тип канала задаёт тип значений: если отправить строку в канал chan int, программа не скомпилируется.

Канал передают в функцию как обычный параметр: func send(ch chan int). Копия канала ссылается на тот же канал, как копия map ссылается на ту же таблицу.

Отправка и приём

С каналом работают две операции. Стрелка показывает, куда идёт значение.

Отправка

Оператор ch <- v отправляет значение v в канал. Стрелка смотрит в сторону канала: значение входит в него.

Приём

Выражение <-ch принимает значение из канала. Стрелка выходит из канала: значение выходит наружу. Принятое значение можно сохранить в переменной: v := <-ch.

Небуферизованный канал

Канал, созданный через make(chan int), называют небуферизованным: он не хранит значений. У такого окна нет полки: повар держит тарелку в руках, пока официант не возьмёт её. Две операции ждут друг друга:

  • отправка ждёт, пока другая горутина не начнёт приём;
  • приём ждёт отправку.

Поэтому в первом примере main обходится без WaitGroup: <-ch ждёт, пока горутина не отправит 42.

Нажмите кнопку и посмотрите, как повар ждёт у окна, пока официант не заберёт суп.

ВживуюКанал без буферабуфер: 0 · ждёт отправитель: 1

Отправитель заблокирован: в канале без буфера значение не хранится. Повар ждёт у окна, пока получатель не возьмёт «суп».

Окно — это канал: через него передают, но не хранят

Значения от одного отправителя приходят в том порядке, в каком он их отправил:

GO
ch := make(chan string)
go func() {
    ch <- "one"
    ch <- "two"
}()
fmt.Println(<-ch, <-ch) // one two

Разберём по шагам:

  1. Первый приём забирает "one".
  2. Пока main не забрала первую тарелку, повар не может подать вторую.
  3. Второй приём забирает "two", и программа печатает one two.

Здесь аналогия с окном заканчивается: на кухне повар и официант — всегда разные люди. В Go отправить и принять значение может одна и та же горутина. Но с небуферизованным каналом она так ждала бы сама себя.

Взаимная блокировка

Горутина на отправке или приёме ждёт, пока другая горутина не выполнит парную операцию. Если ждут все горутины программы, ни одна из них не сдвинется с места. Это взаимная блокировка. Среда выполнения Go замечает её и останавливает программу с ошибкой:

TEXT
fatal error: all goroutines are asleep - deadlock!

Компилятор такую ошибку не находит: сборка проходит, а программа останавливается при запуске.

Канал, равный nil

На собеседовании любят спрашивать, что будет с nil-каналом. Ответ состоит из трёх фактов:

  • канал, объявленный через var ch chan int, равен nil: окно ещё не прорубили;
  • отправка в nil-канал и приём из него ждут вечно;
  • если больше никто не работает, программа остановится с той же ошибкой.

Поэтому канал создают через make.

Частая ошибка

Функцию, которая отправляет значение в канал, вызвали без go:

GO
func send(ch chan int) {
    ch <- 42
}

func main() {
    ch := make(chan int)
    send(ch) // ✗
    fmt.Println(<-ch)
}

main ждёт сама себя:

  1. send выполняется в самой main и на строке ch <- 42 ждёт получателя.
  2. Повар стоит с тарелкой у окна, а официант — он сам.
  3. Приём стоит строкой ниже и так и не начинается, поэтому программа останавливается с fatal error: all goroutines are asleep - deadlock!.

Исправление — go send(ch): тогда отправка ждёт в отдельной горутине, а main доходит до приёма.

Главное

  • Канал передаёт значения между горутинами: ch <- v отправляет, <-ch принимает.
  • Небуферизованный канал не хранит значений: отправка ждёт приёма, а приём — отправки.
  • Когда ждут все горутины, это взаимная блокировка, и программа останавливается при запуске.
  • Отправка в nil-канал и приём из него ждут вечно; канал создают через make.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом