Go: Каналы

35 вопросов

1 Внутреннее устройство канала?

Канал - это структура hchan в рантайме: буфер (кольцевой массив), очереди ожидающих отправителей (sendq) и получателей (recvq), мьютекс для доступа. При операциях горутины блокируются и ставятся в очередь (sudog), при появлении партнёра или места в буфере - пробуждаются.

Открыть отдельно →
2 Чем буферизованный канал отличается от небуферизованного?

Небуферизованный: отправка блокируется, пока кто-то не примет; приём - пока кто-то не отправит. Синхронный обмен. Буферизованный: отправка не блокируется, пока буфер не полон; приём не блокируется, пока буфер не пуст. Асинхронность в пределах размера буфера.

ch := make(chan int)    // unbuffered
ch := make(chan int, 5)  // buffered
Открыть отдельно →
3 Что происходит при чтении из закрытого канала?

Чтение всегда возможно: получаем нулевое значение типа и ok == false. Закрытие канала - способ сигнализировать "данных больше не будет". Многократное чтение из закрытого канала не блокирует и не паникует.

v, ok := <-ch
if !ok { /* channel closed */ }
Открыть отдельно →
4 Что происходит при записи в закрытый канал?

Паника. Закрытый канал нельзя использовать для отправки. Закрывать должен только отправитель (или единственный продюсер), чтобы получатели не писали в закрытый канал.

Открыть отдельно →
5 Что будет при повторном закрытии канала?

Паника. Закрывать канал можно только один раз. Обычно закрытие делают в defer один раз (например, только в продюсере) или используют sync.Once.

Открыть отдельно →
6 Как ведёт себя nil-канал?

Отправка и приём на nil-канале блокируются навсегда. Закрытие nil-канала вызывает панику. Nil-канал в select никогда не срабатывает - его используют для "отключения" ветки (например, при динамическом выборе каналов).

Открыть отдельно →
7 Как работает оператор select?

select выбирает одну из готовых операций (отправка или приём) по нескольким каналам. Если готовы несколько - выбор псевдослучайный. Если ни одна не готова - блокируется. С default не блокируется и выполняет default.

select {
case v := <-ch1: use(v)
case ch2 <- x: 
case <-done: return
default: // non-blocking
}
Открыть отдельно →
8 Зачем default в select?

default делает select неблокирующим: если ни один case не готов, сразу выполняется default. Используют для опроса каналов, таймаутов (в паре с time.After) и неблокирующих отправок/приёмов.

select {
case v := <-ch: return v
default: return 0, ErrNoData
}
Открыть отдельно →
9 Что делает пустой select {}?

Блокируется навсегда. Ни один case нет - ждать нечего. Иногда используют для блокировки main или горутины навсегда (select {} или <-make(chan struct{})).

Открыть отдельно →
10 Кто и как должен закрывать канал?

Закрывает тот, кто больше не будет писать - обычно один продюсер. Получатели не закрывают. При нескольких продюсерах закрытие сложнее: либо один координатор закрывает после сигнала "все закончили", либо канал не закрывают (получатели выходят по другому сигналу).

Открыть отдельно →
11 Какие типы каналов бывают?

Только для отправки: chan<- T. Только для приёма: <-chan T. Двунаправленный: chan T. Направленные типы задают контракт и не позволяют случайно закрыть или писать в канал только для чтения.

func producer(ch chan<- int)
func consumer(ch <-chan int)
Открыть отдельно →
12 Что если писать в полный буфер канала?

Отправка блокируется, пока другая горутина не прочитает из канала. Буфер не растёт - ёмкость фиксирована при создании.

Открыть отдельно →
13 Что если читать из пустого канала?

Приём блокируется, пока другая горутина не отправит в канал (или канал не закроют - тогда получим нуль и ok=false). Для небуферизованного - блокировка до появления отправителя.

Открыть отдельно →
14 Потокобезопасны ли каналы?

Да. Отправка и приём из канала безопасны для одновременного использования из разных горутин. Рантайм синхронизирует доступ внутри. Дополнительный мьютекс для канала не нужен.

Открыть отдельно →
15 Как работает range по каналу?

for v := range ch читает значения из канала, пока он не закрыт. После закрытия цикл завершается. Если канал не закрыть, range будет ждать вечно - типичная причина deadlock.

for v := range ch { process(v) }
Открыть отдельно →
16 Как в select писать в канал?

В case указывают операцию отправки: case ch <- value:. Этот case срабатывает, когда канал готов принять значение (есть место в буфере или есть получатель для небуферизованного).

Открыть отдельно →
17 Буферизованный vs небуферизованный по скорости?

Небуферизованный даёт синхронную передачу - минимум задержки между отправителем и получателем. Буферизованный позволяет отправителю "убежать вперёд" и меньше блокироваться, но добавляет задержку и расход памяти. Выбор по семантике, не только по скорости.

Открыть отдельно →
18 Какие операции возможны с каналом?

Создание: make(chan T) или make(chan T, n). Отправка: ch <- v. Приём: v := <-ch или v, ok := <-ch. Закрытие: close(ch). Длина и ёмкость: len(ch), cap(ch) (для отладки, в коде редко).

Открыть отдельно →
19 GOMAXPROCS=1 и бесконечная горутина, пишущая в канал?

При одном P только одна горутина выполняется. Если одна горутина постоянно пишет в канал и никто не читает (или читатель не успевает), эта горутина заблокируется при полном буфере или при небуферизованном канале. Другая горутина получит время и сможет прочитать - deadlock не обязательно, если есть читатель.

Открыть отдельно →
20 Какой размер буфера у небуферизованного канала?

Ноль. Небуферизованный канал не хранит элементы - передача происходит только при одновременной готовности отправителя и получателя (rendezvous).

Открыть отдельно →
21 Отправка в канал с буфером 1 без читателя?

Отправка успешно выполнится и не заблокируется - в буфере есть место. Значение останется в канале, пока его не прочитают. Если читателя никогда не будет - утечка значения в канале (и возможно утечка горутины, ожидающей следующей отправки).

Открыть отдельно →
22 Несколько отправителей в один канал?

Это нормально и безопасно. Все отправители пишут в один канал; получатель (или несколько) читает. Закрывать канал должен один координатор после того, как все отправители закончат (например, через WaitGroup).

Открыть отдельно →
23 Почему range по каналу может привести к deadlock?

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

Открыть отдельно →
24 Обязательно ли закрывать канал?

Нет. Канал можно не закрывать, если получатели выходят по другому сигналу (контекст, отдельный done-канал). Но если используется for range по каналу - канал нужно закрыть, иначе цикл не завершится. Закрытие - способ сигнала "данных больше не будет".

Открыть отдельно →
25 Как проверить, закрыт ли канал?

Явного API нет. Проверка через приём: v, ok := <-ch; если ok == false, канал закрыт. Нельзя проверять "не читая" - только чтением. Альтернатива - отдельный канал или контекст для сигнала закрытия.

Открыть отдельно →
26 Нюансы Ticker и закрытия канала?

Ticker.Stop() освобождает ресурсы; если не вызвать - утечка. Горутина, читающая из ticker.C, должна учитывать остановку. Не закрывают канал C тикера - его закрывает только Stop(). При отмене контекста нужно остановить тикер и выйти из цикла по тикеру.

ticker := time.NewTicker(interval)
defer ticker.Stop()
for { select { case <-ctx.Done(): return; case <-ticker.C: do() } }
Открыть отдельно →
27 Паттерн done-канала?

Канал done (часто chan struct{}) сигнализирует отмену. Закрытие done - широковещательный сигнал всем: select { case <-done: return }. Либо передают контекст с отменой вместо отдельного канала. Закрывает тот, кто инициирует остановку.

done := make(chan struct{})
go func() { <-sig; close(done) }()
select { case <-done: return; case v := <-ch: ... }
Открыть отдельно →
28 Что такое or-channel (or-done)?

Объединение канала данных с каналом отмены: читаем из того, что готово первым. Либо один канал "or" из нескольких: первый пришедший элемент. Реализация: горутина с select по нескольким каналам пишет в один выходной; при закрытии любого - закрыть выход.

select {
case v := <-ch: return v
case <-done: return zero, errDone
}
Открыть отдельно →
29 Как сделать broadcast через закрытие канала?

Один канал done закрывается один раз - все горутины, ожидающие <-done, одновременно разблокируются. Закрытие - "широковещательный" сигнал. Повторно использовать канал нельзя; для следующего broadcast создают новый канал.

Открыть отдельно →
30 Fan-in через select?

Одна горутина с 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)
Открыть отдельно →
31 Канал как семафор?

Буферизованный канал ёмкостью n: отправка - захват, приём - освобождение. Ограничивает число одновременных операций до n.

sem := make(chan struct{}, 10)
sem <- struct{}{}
defer func() { <-sem }()
Открыть отдельно →
32 Направленные каналы (directional)?

chan<- T - только отправка, <-chan T - только приём. Функция принимает направленный тип и не может случайно закрыть или читать из канала только для записи. Направление задаётся при передаче обычного канала в функцию.

Открыть отдельно →
33 Структуры hchan, sudog, ring buffer в канале?

hchan - дескриптор канала: кольцевой буфер (ring buffer), очереди sendq и recvq из sudog. sudog - обёртка над горутиной в очереди ожидания канала. Ring buffer - фиксированный массив с индексами для очереди элементов в буфере.

Открыть отдельно →
34 Сложность операций с каналом?

Отправка/приём: O(1) при наличии готового партнёра или места в буфере. При блокировке горутина ставится в очередь - тоже O(1). Закрытие - O(1). Реализация без тяжёлых копий - только указатели и очереди.

Открыть отдельно →
35 Когда буферизованный канал приводит к deadlock?

Если все отправители пишут в канал до того, как кто-то начнёт читать, и буфер конечный - отправители блокируются. Если читатель тоже блокируется (ждёт чего-то от отправителей) - взаимная блокировка. Типично: один поток только пишет, другой только читает, но порядок запуска или ещё одна блокировка создают deadlock.

Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.