Каналы в Go
Основные концепции
Каналы — это типизированные "трубы" для передачи данных между горутинами, реализующие модель CSP (Communicating Sequential Processes). Они обеспечивают синхронизацию и обмен данными без явного использования мьютексов.
Базовые характеристики:
- Типизированы — канал может передавать только один тип данных
- Являются ссылочным типом (при передаче в функцию копируется ссылка)
- По умолчанию nil (нулевое значение)
- Создаются через
make(chan Type, [capacity])
Виды каналов
Небуферизированные (синхронные):
ch := make(chan int)
- Отправка блокируется до готовности получателя
- Получение блокируется до готовности отправителя
- Гарантируют синхронизацию: отправитель и получатель встречаются в момент передачи
Буферизированные (асинхронные):
ch := make(chan int, 10)
- Отправка блокируется только при полном буфере
- Получение блокируется только при пустом буфере
- Размер буфера указывается при создании
Направленные каналы:
func send(ch chan<- int, data int) { ch <- data }
func receive(ch <-chan int) int { return <-ch }
chan<-— только для отправки<-chan— только для получения- Проверяются на этапе компиляции
Основные операции
Отправка: ch <- value
- Блокируется, если канал nil (навсегда)
- Блокируется, если небуферизированный и нет получателя
- Блокируется, если буферизированный и полный
- Паникует, если канал закрыт
Получение: value := <-ch
- Блокируется, если канал nil
- Блокируется, если небуферизированный и нет отправителя
- Блокируется, если буферизированный и пустой
- Возвращает нулевое значение, если канал закрыт и пуст
Закрытие: close(ch)
- Только отправитель может закрыть канал
- Повторное закрытие вызывает панику
- Запись в закрытый канал вызывает панику
- Чтение из закрытого канала работает (возвращает нулевое значение)
Паттерны использования
Fan-Out (распределение работы):
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- process(job)
}
}
jobs := make(chan int, 100)
results := make(chan int, 100)
// Запуск воркеров
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Отправка задач
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
Fan-In (объединение результатов):
func merge(chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range chs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Select (мультиплексирование):
select {
case msg1 := <-ch1:
handleMsg1(msg1)
case msg2 := <-ch2:
handleMsg2(msg2)
case <-time.After(1 * time.Second):
handleTimeout()
default:
handleNoData()
}
Типичные ошибки
- Утечка горутин: Отправка в канал, из которого никто не читает
- Deadlock: Взаимная блокировка горутин на каналах
- Горутина не завершается: Не закрыт канал, горутина висит на чтении
- Гонка данных: Использование канала без синхронизации для доступа к разделяемым данным
- Паника при закрытии: Закрытие канала получателем или повторное закрытие
Производительность
- Каналы медленнее мьютексов для простой синхронизации (накладные расходы на аллокации и планировщик)
- Буферизированные каналы эффективнее небуферизированных при высокой нагрузке
- Размер буфера — компромисс между задержкой и потреблением памяти
- Для высоконагруженных систем рассмотрите использование паттерна worker pool
Когда использовать каналы
Подходит:
- Передача владения данными между горутинами
- Сигнализация о событиях (done-каналы)
- Пул воркеров
- Pipeline обработки данных
- Rate limiting
Не подходит:
- Простая защита разделяемых данных (используйте мьютексы)
- Кэширование (используйте sync.Map)
- Синхронизация без передачи данных (WaitGroup проще)