Каналы
Горутина может посчитать значение, но оператор go отбрасывает результат функции. Как повару передать готовое блюдо официанту? На кухне для этого есть окно раздачи: повар ставит тарелку с одной стороны, официант забирает её с другой.
Значение из горутины
В Go такое окно — канал. Одна горутина отправляет в него значения, а другая их принимает:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 6 * 7
}()
fmt.Println(<-ch) // 42
}Давайте разберём пример по строкам:
ch := make(chan int)прорубает окно для значенийint.go func() { … }()запускает горутину-повара: она считает6 * 7и отправляет42в канал.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.
Нажмите кнопку и посмотрите, как повар ждёт у окна, пока официант не заберёт суп.
Отправитель заблокирован: в канале без буфера значение не хранится. Повар ждёт у окна, пока получатель не возьмёт «суп».
Значения от одного отправителя приходят в том порядке, в каком он их отправил:
ch := make(chan string)
go func() {
ch <- "one"
ch <- "two"
}()
fmt.Println(<-ch, <-ch) // one twoРазберём по шагам:
- Первый приём забирает
"one". - Пока
mainне забрала первую тарелку, повар не может подать вторую. - Второй приём забирает
"two", и программа печатаетone two.
Здесь аналогия с окном заканчивается: на кухне повар и официант — всегда разные люди. В Go отправить и принять значение может одна и та же горутина. Но с небуферизованным каналом она так ждала бы сама себя.
Взаимная блокировка
Горутина на отправке или приёме ждёт, пока другая горутина не выполнит парную операцию. Если ждут все горутины программы, ни одна из них не сдвинется с места. Это взаимная блокировка. Среда выполнения Go замечает её и останавливает программу с ошибкой:
fatal error: all goroutines are asleep - deadlock!Компилятор такую ошибку не находит: сборка проходит, а программа останавливается при запуске.
Канал, равный nil
На собеседовании любят спрашивать, что будет с nil-каналом. Ответ состоит из трёх фактов:
- канал, объявленный через
var ch chan int, равенnil: окно ещё не прорубили; - отправка в
nil-канал и приём из него ждут вечно; - если больше никто не работает, программа остановится с той же ошибкой.
Поэтому канал создают через make.
Частая ошибка
Функцию, которая отправляет значение в канал, вызвали без go:
func send(ch chan int) {
ch <- 42
}
func main() {
ch := make(chan int)
send(ch) // ✗
fmt.Println(<-ch)
}main ждёт сама себя:
sendвыполняется в самойmainи на строкеch <- 42ждёт получателя.- Повар стоит с тарелкой у окна, а официант — он сам.
- Приём стоит строкой ниже и так и не начинается, поэтому программа останавливается с
fatal error: all goroutines are asleep - deadlock!.
Исправление — go send(ch): тогда отправка ждёт в отдельной горутине, а main доходит до приёма.
Главное
- Канал передаёт значения между горутинами:
ch <- vотправляет,<-chпринимает. - Небуферизованный канал не хранит значений: отправка ждёт приёма, а приём — отправки.
- Когда ждут все горутины, это взаимная блокировка, и программа останавливается при запуске.
- Отправка в
nil-канал и приём из него ждут вечно; канал создают черезmake.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.