Все главы учебника
Содержание учебника

Каналы

13 мин чтенияКонтент v0.12.1

О чём эта глава

Горутина скачала страницу и должна отдать её парсеру, который работает в другой горутине. Можно положить страницу в общую переменную, защитить её мьютексом и как-то сообщить парсеру, что пора читать. А можно отправить страницу в канал: парсер получит её ровно тогда, когда она готова, и после этого скачивающая горутина к ней больше не прикасается.

Канал — типизированная очередь между горутинами, у которой каждая операция ещё и синхронизирует. Отправка 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 из 2
ch <- 2   // сразу: в буфере 2 из 2
ch <- 3   // ждёт: буфер полон, а получателя нет

Если такой код стоит в main и других горутин нет, третья отправка не дождётся никогда. Рантайм замечает, что все горутины спят и разбудить их некому, и аварийно завершает программу:

fatal error: all goroutines are asleep - deadlock!
 
goroutine 1 [chan send]:
main.main()
	/tmp/demo/main.go:7 +0x38
exit status 2

Строка goroutine 1 [chan send] говорит, где и на какой операции застряла горутина. Это сообщение вы будете видеть часто; читайте его снизу вверх: файл и строка, затем операция в квадратных скобках.

Пример с буфером, которого хватает на все значения:

package main
 
import "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)
	}
}
отправил 1 len/cap: 1 / 3
отправил 4 len/cap: 2 / 3
отправил 9 len/cap: 3 / 3
прочитал 1
прочитал 4
прочитал 9

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 main
 
import (
	"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 main
 
import "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)
}
сумма: 10

nil-каналы

Отправка в nil-канал и приём из него ждут вечно:

var ch chan int   // nil: канал объявлен, но не создан через make
<-ch              // ждёт вечно
ch <- 1           // ждёт вечно

Сама по себе такая операция бесполезна, но внутри select это выключатель. Ветка с nil-каналом никогда не будет выбрана. Присвоив переменной канала nil, вы убираете ветку из рассмотрения, не меняя код select.

Типичный случай: читаем два канала, и один из них закрылся. Приём из закрытого канала не ждёт, поэтому select выбирал бы эту ветку снова и снова, получая нули. Если обнулить переменную закрытого канала, select будет ждать только второй:

package main
 
import "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 main
 
import "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))
}
итог: 6

Частые ошибки

Отправка в закрытый канал. Чаще всего бывает, когда отправителей несколько и один из них закрыл канал, пока другие ещё работают. Решение — один владелец закрытия, который точно знает, что все отправители закончили.

Горутина зависла на отправке. Получатель ушёл, а горутина ждёт на 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 <- 10
ch <- 20
close(ch)
for i := 0; i < 3; i++ {
    v, ok := <-ch
    fmt.Println(v, ok)
}
10 true
20 true
0 false

Буфера хватает на обе отправки, поэтому горутина не нужна. Закрытие не удаляет значения, уже лежащие в буфере. Все следующие приёмы тоже вернут 0 false, а отправка после close вызовет панику.

Что дальше

Главный навык для задач блока 1 и блока 3 — для каждого канала уметь ответить, кто его закрывает и кто гарантированно дочитает. Если на оба вопроса есть ответ, конкурентный код почти наверняка завершится корректно.

Следующая глава, select, учит ждать сразу несколько каналов и добавлять к ожиданию таймаут и отмену. Дальше каналы встретятся в context (сигнал отмены — это закрытый канал) и в паттернах, где из них собирают конвейеры и пулы.

Продолжить

Остались вопросы?