Буферизация и итерация по каналам через 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 вашего бэкенда еще безопаснее.