← Назад к вопросам

Как устроены каналы?

2.0 Middle🔥 231 комментариев
#Конкурентность и горутины

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Устройство каналов в Go

Канал — это механизм синхронизации и передачи данных между горутинами. Каналы позволяют безопасно обмениваться информацией без явных блокировок и мьютексов.

Внутренняя структура канала

Внутренне канал представлен структурой с несколькими полями:

type hchan struct {
    qcount   uint           // количество элементов в буфере
    dataqsiz uint           // размер буфера (capacity)
    buf      unsafe.Pointer // указатель на массив буфера
    elemsize uint16         // размер одного элемента
    closed   uint32         // флаг закрытия канала
    elemtype *_type         // тип элементов канала
    sendx    uint           // индекс в буфере для отправки
    recvx    uint           // индекс в буфере для получения
    recvq    waitq          // очередь горутин, ждущих приёма
    sendq    waitq          // очередь горутин, ждущих отправки
    lock     mutex          // мьютекс для синхронизации
}

Ненаправленные каналы (Bidirectional)

// Создание канала
ch := make(chan int)        // без буфера (синхронный)
ch := make(chan int, 5)     // с буфером на 5 элементов (асинхронный)

// Отправка
ch <- 42

// Получение
value := <-ch

Направленные каналы (Directional)

// Send-only канал
sendCh := make(chan<- int)

// Receive-only канал
recvCh := make(<-chan int)

// Конвертация между типами
var sendOnly chan<- int
var recvOnly <-chan int
var bidir chan int

sendOnly = bidir  // OK: ненаправленный в send-only
recvOnly = bidir  // OK: ненаправленный в recv-only
// bidir = sendOnly // ОШИБКА: не можно обратно

Механизм блокировки

Каналы без буфера (синхронные):

ch := make(chan int) // без буфера

go func() {
    ch <- 42  // Заблокируется, пока кто-то не прочитает
}()

value := <-ch // Разблокирует отправку, получит 42
fmt.Println(value) // 42

Каналы с буфером (асинхронные):

ch := make(chan int, 2) // буфер на 2 элемента

ch <- 1  // OK, буфер не полон
ch <- 2  // OK, буфер не полон
// ch <- 3  // Заблокируется: буфер полон

v1 := <-ch // 1
v2 := <-ch // 2
// v3 := <-ch // Заблокируется: буфер пуст

Закрытие канала

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

close(ch)  // Закрываем канал

// После закрытия:
// - Получение всегда успешно (пока буфер не пуст, потом вернёт нулевое значение)
// - Отправка вызовет панику
// - Проверка закрытия

value, ok := <-ch
if !ok {
    fmt.Println("Канал закрыт")
}

Итерация по каналу

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

for value := range ch {
    fmt.Println(value) // 1, 2, 3
}
// После того как канал закрыт и буфер пуст, цикл завершается

Select для множественных каналов

ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(time.Second)
    ch1 <- "one"
}()

go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "two"
}()

for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("Received", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received", msg2)
    }
}

Практические паттерны

Паттерн Done сигнал:

func worker(done <-chan bool) {
    for {
        select {
        case <-done:
            return
        default:
            // работа
        }
    }
}

Пулинг результатов:

results := make(chan int, 10)

for i := 0; i < 10; i++ {
    go func(n int) {
        results <- n * 2
    }(i)
}

for i := 0; i < 10; i++ {
    fmt.Println(<-results)
}

Важные моменты

  • Каналы потокобезопасны за счёт внутреннего мьютекса
  • Нельзя отправлять в closed канал (паника)
  • Приём из closed канала всегда успешен (нулевое значение)
  • nil канал вызывает deadlock при операциях
  • Закрывать канал должен отправитель, не получатель

Каналы — основной инструмент для безопасной конкурентности в Go.

Как устроены каналы? | PrepBro