35 вопросов
Канал - это структура hchan в рантайме: буфер (кольцевой массив), очереди ожидающих отправителей (sendq) и получателей (recvq), мьютекс для доступа. При операциях горутины блокируются и ставятся в очередь (sudog), при появлении партнёра или места в буфере - пробуждаются.
Небуферизованный: отправка блокируется, пока кто-то не примет; приём - пока кто-то не отправит. Синхронный обмен. Буферизованный: отправка не блокируется, пока буфер не полон; приём не блокируется, пока буфер не пуст. Асинхронность в пределах размера буфера.
ch := make(chan int) // unbuffered
ch := make(chan int, 5) // bufferedЧтение всегда возможно: получаем нулевое значение типа и ok == false. Закрытие канала - способ сигнализировать "данных больше не будет". Многократное чтение из закрытого канала не блокирует и не паникует.
v, ok := <-ch
if !ok { /* channel closed */ }Паника. Закрытый канал нельзя использовать для отправки. Закрывать должен только отправитель (или единственный продюсер), чтобы получатели не писали в закрытый канал.
Паника. Закрывать канал можно только один раз. Обычно закрытие делают в defer один раз (например, только в продюсере) или используют sync.Once.
Отправка и приём на nil-канале блокируются навсегда. Закрытие nil-канала вызывает панику. Nil-канал в select никогда не срабатывает - его используют для "отключения" ветки (например, при динамическом выборе каналов).
select выбирает одну из готовых операций (отправка или приём) по нескольким каналам. Если готовы несколько - выбор псевдослучайный. Если ни одна не готова - блокируется. С default не блокируется и выполняет default.
select {
case v := <-ch1: use(v)
case ch2 <- x:
case <-done: return
default: // non-blocking
}default делает select неблокирующим: если ни один case не готов, сразу выполняется default. Используют для опроса каналов, таймаутов (в паре с time.After) и неблокирующих отправок/приёмов.
select {
case v := <-ch: return v
default: return 0, ErrNoData
}Блокируется навсегда. Ни один case нет - ждать нечего. Иногда используют для блокировки main или горутины навсегда (select {} или <-make(chan struct{})).
Закрывает тот, кто больше не будет писать - обычно один продюсер. Получатели не закрывают. При нескольких продюсерах закрытие сложнее: либо один координатор закрывает после сигнала "все закончили", либо канал не закрывают (получатели выходят по другому сигналу).
Только для отправки: chan<- T. Только для приёма: <-chan T. Двунаправленный: chan T. Направленные типы задают контракт и не позволяют случайно закрыть или писать в канал только для чтения.
func producer(ch chan<- int)
func consumer(ch <-chan int)Отправка блокируется, пока другая горутина не прочитает из канала. Буфер не растёт - ёмкость фиксирована при создании.
Приём блокируется, пока другая горутина не отправит в канал (или канал не закроют - тогда получим нуль и ok=false). Для небуферизованного - блокировка до появления отправителя.
Да. Отправка и приём из канала безопасны для одновременного использования из разных горутин. Рантайм синхронизирует доступ внутри. Дополнительный мьютекс для канала не нужен.
for v := range ch читает значения из канала, пока он не закрыт. После закрытия цикл завершается. Если канал не закрыть, range будет ждать вечно - типичная причина deadlock.
for v := range ch { process(v) }В case указывают операцию отправки: case ch <- value:. Этот case срабатывает, когда канал готов принять значение (есть место в буфере или есть получатель для небуферизованного).
Небуферизованный даёт синхронную передачу - минимум задержки между отправителем и получателем. Буферизованный позволяет отправителю "убежать вперёд" и меньше блокироваться, но добавляет задержку и расход памяти. Выбор по семантике, не только по скорости.
Создание: make(chan T) или make(chan T, n). Отправка: ch <- v. Приём: v := <-ch или v, ok := <-ch. Закрытие: close(ch). Длина и ёмкость: len(ch), cap(ch) (для отладки, в коде редко).
При одном P только одна горутина выполняется. Если одна горутина постоянно пишет в канал и никто не читает (или читатель не успевает), эта горутина заблокируется при полном буфере или при небуферизованном канале. Другая горутина получит время и сможет прочитать - deadlock не обязательно, если есть читатель.
Ноль. Небуферизованный канал не хранит элементы - передача происходит только при одновременной готовности отправителя и получателя (rendezvous).
Отправка успешно выполнится и не заблокируется - в буфере есть место. Значение останется в канале, пока его не прочитают. Если читателя никогда не будет - утечка значения в канале (и возможно утечка горутины, ожидающей следующей отправки).
Это нормально и безопасно. Все отправители пишут в один канал; получатель (или несколько) читает. Закрывать канал должен один координатор после того, как все отправители закончат (например, через WaitGroup).
Range читает до закрытия канала. Если продюсер не закроет канал после отправки всех данных, получатель на range будет ждать вечно. Если продюсер ждёт чего-то от получателя - взаимная блокировка. Решение: закрывать канал после последней отправки.
Нет. Канал можно не закрывать, если получатели выходят по другому сигналу (контекст, отдельный done-канал). Но если используется for range по каналу - канал нужно закрыть, иначе цикл не завершится. Закрытие - способ сигнала "данных больше не будет".
Явного API нет. Проверка через приём: v, ok := <-ch; если ok == false, канал закрыт. Нельзя проверять "не читая" - только чтением. Альтернатива - отдельный канал или контекст для сигнала закрытия.
Ticker.Stop() освобождает ресурсы; если не вызвать - утечка. Горутина, читающая из ticker.C, должна учитывать остановку. Не закрывают канал C тикера - его закрывает только Stop(). При отмене контекста нужно остановить тикер и выйти из цикла по тикеру.
ticker := time.NewTicker(interval)
defer ticker.Stop()
for { select { case <-ctx.Done(): return; case <-ticker.C: do() } }Канал done (часто chan struct{}) сигнализирует отмену. Закрытие done - широковещательный сигнал всем: select { case <-done: return }. Либо передают контекст с отменой вместо отдельного канала. Закрывает тот, кто инициирует остановку.
done := make(chan struct{})
go func() { <-sig; close(done) }()
select { case <-done: return; case v := <-ch: ... }Объединение канала данных с каналом отмены: читаем из того, что готово первым. Либо один канал "or" из нескольких: первый пришедший элемент. Реализация: горутина с select по нескольким каналам пишет в один выходной; при закрытии любого - закрыть выход.
select {
case v := <-ch: return v
case <-done: return zero, errDone
}Один канал done закрывается один раз - все горутины, ожидающие <-done, одновременно разблокируются. Закрытие - "широковещательный" сигнал. Повторно использовать канал нельзя; для следующего broadcast создают новый канал.
Одна горутина с select по нескольким входным каналам читает первый пришедший элемент и пишет в один выходной канал. Все входы в итоге нужно прочитать до конца и закрыть выход после завершения всех.
for ch1 != nil || ch2 != nil {
select {
case v, ok := <-ch1: if !ok { ch1 = nil }; else { out <- v }
case v, ok := <-ch2: if !ok { ch2 = nil }; else { out <- v }
}
}
close(out)Буферизованный канал ёмкостью n: отправка - захват, приём - освобождение. Ограничивает число одновременных операций до n.
sem := make(chan struct{}, 10)
sem <- struct{}{}
defer func() { <-sem }()chan<- T - только отправка, <-chan T - только приём. Функция принимает направленный тип и не может случайно закрыть или читать из канала только для записи. Направление задаётся при передаче обычного канала в функцию.
hchan - дескриптор канала: кольцевой буфер (ring buffer), очереди sendq и recvq из sudog. sudog - обёртка над горутиной в очереди ожидания канала. Ring buffer - фиксированный массив с индексами для очереди элементов в буфере.
Отправка/приём: O(1) при наличии готового партнёра или места в буфере. При блокировке горутина ставится в очередь - тоже O(1). Закрытие - O(1). Реализация без тяжёлых копий - только указатели и очереди.
Если все отправители пишут в канал до того, как кто-то начнёт читать, и буфер конечный - отправители блокируются. Если читатель тоже блокируется (ждёт чего-то от отправителей) - взаимная блокировка. Типично: один поток только пишет, другой только читает, но порядок запуска или ещё одна блокировка создают deadlock.