Отправка и получение
Канал это типизированная труба между горутинами. ch <- v отправляет, <-ch получает. Создаётся канал через make:
Нулевое значение канального типа это nil, поэтому var ch chan string без make даёт канал, который блокируется навсегда. Всегда создавайте каналы через make.
Небуферизованные каналы синхронизируют
make(chan T) создаёт небуферизованный канал. Отправка блокируется, пока получатель не заберёт значение, а получение блокируется, пока отправитель его не даст. Две горутины встречаются в этой точке, поэтому небуферизованный канал в той же мере инструмент синхронизации, что и труба для данных. Когда ниже возвращается <-done, вы знаете, что воркер закончил всё, что было до его отправки:
Читать result в main здесь безопасно и без мьютекса. Модель памяти Go гарантирует, что всё, что воркер сделал до отправки, видно main после соответствующего получения. chan struct{} это идиоматичный тип для чистого сигнала, потому что struct{} не занимает памяти.
Буферизованные каналы
make(chan T, n) даёт каналу место под n значений. Отправки проходят без получателя, пока буфер не заполнится; получения проходят, пока он не опустеет. Значения выходят в том же порядке, в каком вошли.
Буфер развязывает отправителя и получателя, чтобы короткие всплески не останавливали отправителя. Он не спасает, если производитель постоянно быстрее потребителя; он только откладывает момент, когда отправитель заблокируется. Выбирайте размер буфера по причине (число отправителей, известный размер пачки), а не чтобы убрать deadlock.
len(ch) это снимок состояния. К тому моменту, когда вы на него отреагируете, другая горутина может его изменить, поэтому не используйте его, чтобы решить, заблокируется ли отправка. Для этого есть select с веткой default.
close и range
close(ch) сообщает получателям, что значений больше отправлено не будет. После закрытия:
- значения, уже лежащие в буфере, всё равно доставляются,
- затем каждое получение сразу возвращает нулевое значение,
v, ok := <-chсообщаетok == false,for v := range chзавершается.
Первое получение после close всё ещё даёт "last" с ok == true. Второе даёт нулевое значение "" и false.
Правила, нарушение которых вызывает панику:
- отправка в закрытый канал паникует с
send on closed channel, - закрытие уже закрытого канала паникует,
- закрытие
nil-канала паникует.
Поэтому закрывает только отправляющая сторона, и только один раз. Когда отправителей несколько, ни один из них не знает, закончили ли остальные; пусть отдельная горутина дождётся всех отправителей (через sync.WaitGroup) и закроет канал после возврата Wait. Закрывать нужно, только когда получатель ждёт конца. Незакрытый канал, на который никто не ссылается, собирается сборщиком мусора, как любое другое значение.
Типы с направлением
Функция может объявить, что она только отправляет или только получает из канала. Тогда компилятор отвергнет другую операцию.
| Тип | Значение | Разрешено |
|---|---|---|
chan T | двунаправленный | отправка, получение, закрытие |
chan<- T | только отправка | отправка, закрытие |
<-chan T | только получение | получение |
chan T неявно преобразуется в любой из ограниченных типов при передаче в функцию. Поэтому produce выше может вернуть <-chan int: вызывающий код может обходить его через range, но не может ни отправить в него, ни закрыть его. Используйте типы с направлением во всех параметрах функций, где это применимо. Они документируют владение и превращают неправильное использование в ошибку компиляции.
Deadlock
Если каждая горутина заблокирована и ничто не может разбудить ни одну из них, среда выполнения прерывает программу:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
Дамп горутин говорит, какая операция застряла (здесь chan send, а бывает chan receive, sync.WaitGroup.Wait, select). Обычные причины:
- отправка в небуферизованный канал, когда не запущен ни один получатель,
rangeпо каналу, который никогда не закрывается,- счётчик
WaitGroup, который никогда не доходит до нуля, - две горутины, каждая из которых ждёт другую.
Среда выполнения распознаёт только случай, когда застряли все горутины. В сервере, где живы другие горутины (HTTP-слушатель, тикер), та же ошибка ничего не роняет; заблокированная горутина просто утекает.
nil-каналы
Отправка и получение на nil-канале блокируются навсегда. Звучит бесполезно, но это стандартный приём внутри select: присвоив переменной канала nil, вы отключаете его ветку. Этот код объединяет два канала и перестаёт слушать каждый из них, как только тот закрыт:
Без присваиваний nil закрытый канал всегда готов, и цикл крутился бы на нулевых значениях.
Конвейер
Каналы складываются в конвейеры: каждая стадия это горутина, которая получает из одного канала, отправляет в следующий и закрывает свой выход, когда её вход закончился.
Все стадии работают конкурентно, а порядок вывода детерминирован (1, 16, 81), потому что каждая стадия это одна горутина, сохраняющая порядок. Закрытия идут каскадом: generate закрывается, это завершает range в square, та закрывает свой выход, и так далее до main.
Слабое место этого конвейера: если main перестанет читать раньше времени, стадии навсегда заблокируются на отправке. Настоящие конвейеры принимают context.Context или канал done и делают select по нему рядом с каждой отправкой.
Канал или мьютекс
Каналы нужны для передачи владения данными и для сигнализации о событиях. sync.Mutex проще для защиты общего состояния, которое многие горутины читают и обновляют на месте, например кеша или счётчика. Структура с мьютексом внутри часто понятнее, чем горутина, которая владеет состоянием и обслуживает запросы через каналы. Используйте то, что делает код короче, а владение очевидным.
Шпаргалка
| Операция | nil-канал | открытый канал | закрытый канал |
|---|---|---|---|
ch <- v | блокируется навсегда | блокируется, пока значение не получат или в буфере не будет места | паника |
<-ch | блокируется навсегда | блокируется, пока не появится значение | значения из буфера, затем нулевое значение |
v, ok := <-ch | блокируется навсегда | ok равно true | ok равно false, когда буфер опустел |
close(ch) | паника | закрывает | паника |
len(ch), cap(ch) | 0, 0 | значений в буфере, размер буфера | оставшихся значений, размер буфера |
Часто задаваемые вопросы
Чем буферизованный канал отличается от небуферизованного в Go?
У небуферизованного канала (make(chan int)) нет хранилища: отправка блокируется, пока другая горутина не получит значение, так что каждая отправка это одновременно передача из рук в руки и точка синхронизации. Буферизованный канал (make(chan int, 3)) вмещает до 3 значений; отправка блокируется, только когда буфер полон, а получение, только когда он пуст.
Что происходит при чтении из закрытого канала в Go?
Получение из закрытого канала никогда не блокируется. Сначала оно выдаёт значения, оставшиеся в буфере, а затем бесконечно возвращает нулевое значение типа элементов. Чтобы различить эти случаи, используйте v, ok := <-ch: ok равно false, когда канал закрыт и пуст. Цикл for v := range ch в этот момент останавливается.
Кто должен закрывать канал в Go?
Отправитель, и только когда получателям нужно знать, что значений больше не будет (например, чтобы завершить цикл range). Отправка в закрытый канал вызывает панику, как и повторное закрытие, поэтому получатель, закрывающий канал, вступает в гонку с отправителями. Закрывать канал, чтобы освободить его, не нужно; сборщик мусора в любом случае заберёт недостижимые каналы.
Что означает «fatal error: all goroutines are asleep»?
Все горутины программы заблокированы на операции с каналом или блокировке, которую уже ничто не может завершить, и среда выполнения останавливает программу. Самая частая причина: отправка в небуферизованный канал в main, когда нет другой горутины-получателя, или range по каналу, который никогда не закрывается.