Буферизация и итерация по каналам через range

Мы переходим к одной из самых захватывающих тем в Go. Если горутины — это рабочие руки приложения, то каналы (Channel) — это конвейер, который позволяет им безопасно обмениваться данными.

В React и Node.js вы привыкли к асинхронности через Event Loop и Promise. В Go подход иной: «Не общайтесь через разделяемую память, разделяйте память через общение». Channel — это типизированный поток, по которому одна горутина передает значение другой.

Синхронное рандеву: Unbuffered channel

По умолчанию каналы в Go — небуферизированные (Unbuffered channel). У них нет внутреннего хранилища. Чтобы передача данных состоялась, отправитель и получатель должны встретиться в одной точке кода одновременно.

Это создает эффект Blocking operation (блокирующая операция):

  • Отправитель «засыпает», пока получатель не заберет данные.
  • Получатель ждет, пока отправитель не положит данные в канал.
package main

import "fmt"

func main() {
    // Создаем небуферизированный канал для строк
    messages := make(chan string)

    go func() {
        // Горутина заблокируется здесь, пока main не прочитает данные
        messages <- "Привет от горутины!"
        fmt.Println("Данные отправлены")
    }()

    // Main заблокируется здесь в ожидании данных
    msg := <-messages
    fmt.Println("Получено:", msg)
}

Как показано на Схеме 1, Unbuffered channel работает как передача эстафетной палочки из рук в руки.

Буферизированные каналы: управление нагрузкой

Если вы не хотите, чтобы отправитель ждал получателя мгновенно, используйте Buffered channel. При создании такого канала указывается емкость (capacity).

Буфер работает как очередь: отправитель кладет данные и продолжает работу. Однако, когда буфер заполняется, следующая запись снова становится Blocking operation.

В бэкенде это инструмент для реализации Backpressure (обратного давления). Ограничивая буфер, вы контролируете потребление памяти и не даете системе «захлебнуться» под лавиной задач.

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

jobs <- 1 // Не блокируется
jobs <- 2 // Не блокируется
// jobs <- 3 // Заблокирует горутину: буфер полон

Итерация и закрытие канала

В TypeScript для массивов есть for...of. В Go для чтения из каналов используют range. Цикл запрашивает данные, пока канал не будет закрыт.

Для завершения передачи используйте встроенную функцию close(). Это сигнал, что данных больше не будет.

Правило владения: Канал закрывает тот, кто в него пишет. Запись в закрытый канал вызовет панику (panic), а чтение из закрытого канала всегда возвращает Zero value и не блокирует поток. ⚓

func producer(dataChan chan int) {
    for i := 1; i <= 3; i++ {
        dataChan <- i
    }
    close(dataChan) // Сигнализируем об окончании
}

func main() {
    ch := make(chan int, 3)
    go producer(ch)

    for val := range ch {
        fmt.Println("Обработано:", val)
    }
    fmt.Println("Работа завершена")
}

Практический кейс: Сборщик логов

Представьте: микросервис пишет логи. Синхронная запись в файл замедлит ответы пользователям. Используем Buffered channel как промежуточную очередь.

package main

import (
    "fmt"
    "time"
)

func logger(logs chan string) {
    for msg := range logs {
        // Имитируем долгую запись на диск
        time.Sleep(500 * time.Millisecond)
        fmt.Printf("[LOG]: %s\n", msg)
    }
}

func main() {
    // Буфер позволяет логике не ждать записи лога
    logChan := make(chan string, 10)

    go logger(logChan)

    logChan <- "User login"
    logChan <- "API request success"
    
    time.Sleep(2 * time.Second)
}

Если забыть закрыть канал при итерации через range, возникнет Deadlock (взаимная блокировка). Рантайм Go обнаружит это и аварийно завершит программу — это защитный механизм против утечек горутин. 🛡️

Мы научились передавать данные и управлять очередями. В следующей теме разберем, как использовать систему типов для создания направленных каналов, чтобы сделать API вашего бэкенда еще безопаснее.