Горутина скачала страницу и должна отдать её парсеру, который работает в другой
горутине. Можно положить страницу в общую переменную, защитить её мьютексом и как-то
сообщить парсеру, что пора читать. А можно отправить страницу в канал: парсер получит
её ровно тогда, когда она готова, и после этого скачивающая горутина к ней больше не
прикасается.
Канал — типизированная очередь между горутинами, у которой каждая операция ещё и
синхронизирует. Отправка happens-before соответствующего приёма (см.
модель памяти), поэтому всё, что отправитель записал до
отправки, получатель гарантированно видит после приёма. Отсюда девиз Go: «Don't
communicate by sharing memory; share memory by communicating» — не общайтесь через
общую память, а передавайте память через общение.
В главе четыре темы, на которых держится почти весь конкурентный Go: буфер и его
отсутствие, закрытие канала как сигнал всем получателям, nil-канал и направления
каналов в сигнатурах.
Нужны go, make, срезы, циклы и функции с несколькими результатами. Базовый
синтаксис канала уже был во вводной главе:
ch := make(chan int) // небуферизованный каналch := make(chan int, 100) // канал с буфером на 100 элементовch <- 42 // отправкаv := <-ch // приёмv, ok := <-ch // ok == false, если канал закрыт и пуст
Читая главу, держите в голове три вопроса к каждому каналу: когда операция с ним
ждёт, как получатель узнаёт, что данных больше не будет, и кто имеет право канал
закрыть. Создать канал и отвечать за его закрытие — разные роли: функция может
создать канал и отдать право закрытия горутине-отправителю.
Пример с выключением веток через nil использует select из следующей главы. Его
можно пропустить и вернуться к нему вместе с select.
От размера буфера зависит, когда именно горутина остановится на операции с каналом.
Небуферизованный канал (make(chan T)) — точка встречи. Отправка не кладёт значение
в очередь: она ждёт, пока кто-то начнёт принимать. Приём ждёт, пока кто-то отправит.
Значение переходит из рук в руки в момент встречи. Поэтому после успешной отправки
отправитель знает, что получатель значение уже забрал.
Буферизованный канал (make(chan T, n)) разрешает отправить до n значений, не
дожидаясь получателя. Отправка ждёт только при полном буфере, приём — только при
пустом. Буфер сглаживает рывки: если отправитель на мгновение обогнал получателя,
значения подождут в буфере.
ch := make(chan int, 2)ch <- 1 // сразу: в буфере 1 из 2ch <- 2 // сразу: в буфере 2 из 2ch <- 3 // ждёт: буфер полон, а получателя нет
Если такой код стоит в main и других горутин нет, третья отправка не дождётся
никогда. Рантайм замечает, что все горутины спят и разбудить их некому, и аварийно
завершает программу:
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:main.main() /tmp/demo/main.go:7 +0x38exit status 2
Строка goroutine 1 [chan send] говорит, где и на какой операции застряла горутина.
Это сообщение вы будете видеть часто; читайте его снизу вверх: файл и строка, затем
операция в квадратных скобках.
Пример с буфером, которого хватает на все значения:
package mainimport "fmt"func main() { ch := make(chan int, 3) for i := 1; i <= 3; i++ { ch <- i * i // не ждём: место в буфере есть fmt.Println("отправил", i*i, "len/cap:", len(ch), "/", cap(ch)) } close(ch) for v := range ch { fmt.Println("прочитал", v) }}
cap(ch) — размер буфера, len(ch) — сколько значений лежит в нём сейчас. Для
отладки удобно, но принимать решения по len(ch) в конкурентном коде нельзя: к
следующей строке значение уже может измениться.
Буфер не ускоряет обработку и не отменяет того, что кто-то должен значения прочитать.
Его размер выбирают осознанно. Буфер 1 часто ставят, чтобы горутина-отправитель могла
отдать единственный результат и завершиться, даже если получатель уже ушёл по
таймауту; этот приём подробно разобран в главе
«Утечки горутин и гонки». Большой буфер, наоборот,
настораживает: он прячет ситуацию «получатель не успевает». Отправитель узнаёт о
проблеме не сразу, а когда очередь уже раздулась.
close(ch) означает «данных больше не будет». Из закрытого канала приём никогда не
ждёт: сначала он отдаёт значения, оставшиеся в буфере (с ok == true), а когда буфер
опустел — нулевое значение типа и ok == false.
done := make(chan struct{})close(done)<-done // не ждёт<-done // и снова не ждёт: закрытие видно всем и навсегда
Отсюда главное свойство: одно закрытие будит всех горутин, ждущих приёма. Отправка
будит одного получателя, а close — сколько угодно. На этом построены сигнальные
каналы done и весь пакет context. Для чистого сигнала берут тип
chan struct{}: значение не несёт данных и не занимает памяти.
Три воркера ждут одного сигнала старта:
package mainimport ( "fmt" "sync")func main() { start := make(chan struct{}) var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() <-start // ждём общего сигнала fmt.Println("воркер", id, "поехал") }(i) } close(start) // один close будит всех wg.Wait() fmt.Println("все воркеры стартовали")}
Все три строки «воркер N поехал» будут напечатаны, но в каком порядке — не определено.
все воркеры стартовали всегда последняя: её печатает main после wg.Wait().
Нарушение правил закрытия вызывает панику:
закрыть уже закрытый канал — panic: close of closed channel;
отправить в закрытый канал — panic: send on closed channel;
закрыть nil-канал — panic: close of nil channel.
Из этого правило: канал закрывает отправитель, и только один. Получатель не знает,
закончили ли отправители, и закрывать канал не должен. Если отправителей несколько,
закрытие поручают координатору: он ждёт, пока все отправители закончат (обычно через
WaitGroup), и закрывает канал сам. Этот приём называется fan-in и подробно разобран
в главе «Паттерны». sync.Once вокруг close спасает только от
повторного закрытия, но не от отправки в уже закрытый канал.
for range по каналу читает значения, пока канал не закрыт и не опустошён, а потом
выходит из цикла. Это самый частый способ дочитать отправителя до конца:
package mainimport "fmt"func main() { nums := make(chan int) go func() { for i := 1; i <= 4; i++ { nums <- i } close(nums) // без этого range ждал бы вечно }() sum := 0 for v := range nums { // выходит, когда канал закрыт и пуст sum += v } fmt.Println("сумма:", sum)}
var ch chan int // nil: канал объявлен, но не создан через make<-ch // ждёт вечноch <- 1 // ждёт вечно
Сама по себе такая операция бесполезна, но внутри select это
выключатель. Ветка с nil-каналом никогда не будет выбрана. Присвоив переменной
канала nil, вы убираете ветку из рассмотрения, не меняя код select.
Типичный случай: читаем два канала, и один из них закрылся. Приём из закрытого канала
не ждёт, поэтому select выбирал бы эту ветку снова и снова, получая нули. Если
обнулить переменную закрытого канала, select будет ждать только второй:
package mainimport "fmt"func main() { a := make(chan int, 2) b := make(chan int, 2) a <- 1 a <- 2 close(a) b <- 10 b <- 20 close(b) sum := 0 for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil // ветка a больше не будет выбрана continue } sum += v case v, ok := <-b: if !ok { b = nil continue } sum += v } } fmt.Println("слили оба канала, сумма:", sum)}
слили оба канала, сумма: 33
Цикл продолжается, пока жив хотя бы один источник. Когда оба канала обнулены, условие
a != nil || b != nil ложно и цикл заканчивается.
В сигнатуре функции канал можно сузить до «только отправка» или «только приём»:
func produce(out chan<- int) { out <- 1 } // chan<- int: только отправлятьfunc consume(in <-chan int) { <-in } // <-chan int: только принимать
Стрелка показывает, куда движутся данные относительно канала. Обычный chan int
автоматически приводится к chan<- int или <-chan int при передаче в функцию, а
обратно — нет. Компилятор не даст функции-потребителю отправить в канал или закрыть
его: закрыть можно только двунаправленный канал или канал «только для отправки». Так
роли видны прямо из сигнатуры.
package mainimport "fmt"func produce(out chan<- int) { for i := 1; i <= 3; i++ { out <- i } close(out)}func sum(in <-chan int) int { total := 0 for v := range in { total += v } return total}func main() { ch := make(chan int) go produce(ch) // chan int подходит как chan<- int fmt.Println("итог:", sum(ch))}
Отправка в закрытый канал. Чаще всего бывает, когда отправителей несколько и один
из них закрыл канал, пока другие ещё работают. Решение — один владелец закрытия,
который точно знает, что все отправители закончили.
Горутина зависла на отправке. Получатель ушёл, а горутина ждёт на ch <- v вечно и
держит свой стек и всё, на что ссылается:
func leak() chan int { ch := make(chan int) go func() { ch <- expensive() }() // если результат не прочитают, горутина не завершится return ch}
Варианты исправления: буфер 1 (единственный результат ляжет в буфер), сигнал отмены в
select рядом с отправкой или гарантия, что получатель всегда дочитывает канал.
Закрытие со стороны получателя. Получатель закрывает канал «на всякий случай», а
живой отправитель на следующей отправке паникует. Получатель канал не закрывает.
for range по каналу, который никто не закроет. Цикл будет ждать следующего значения
вечно. Если канал читают через range, закрытие — обязательная часть протокола, и в
коде должно быть видно, кто его делает.
У каждого канала три состояния, и каждое отвечает на свой вопрос:
Открытый канал передаёт значения, а вместе с ними — видимость всего, что
отправитель записал до отправки. Владение данными канал при этом не передаёт
автоматически: если вы отправили указатель, срез или карту, договоритесь, что
отправитель после отправки их не меняет.
Закрытый канал говорит «событий больше не будет» всем получателям сразу и
навсегда.
nil-канал никогда не срабатывает; в select это выключенная ветка.
Почти любой дедлок, утечка или паника на канале объясняется одним из этих трёх
состояний.
Создайте канал int с буфером 2, отправьте 10 и 20, закройте его. До запуска
запишите результаты трёх подряд приёмов v, ok := <-ch. Потом проверьте кодом.
Понадобится ли горутина для двух первых отправок?
ch := make(chan int, 2)ch <- 10ch <- 20close(ch)for i := 0; i < 3; i++ { v, ok := <-ch fmt.Println(v, ok)}
10 true20 true0 false
Буфера хватает на обе отправки, поэтому горутина не нужна. Закрытие не удаляет
значения, уже лежащие в буфере. Все следующие приёмы тоже вернут 0 false, а
отправка после close вызовет панику.
Главный навык для задач блока 1 и блока 3 — для каждого канала уметь
ответить, кто его закрывает и кто гарантированно дочитает. Если на оба вопроса
есть ответ, конкурентный код почти наверняка завершится корректно.
Следующая глава, select, учит ждать сразу несколько каналов и
добавлять к ожиданию таймаут и отмену. Дальше каналы встретятся в
context (сигнал отмены — это закрытый канал) и в
паттернах, где из них собирают конвейеры и пулы.