Каналы - связь между горутинами
"Don't communicate by sharing memory; share memory by communicating" - философия Go. Каналы - это типизированные трубы для передачи данных между горутинами.
Основы каналов
Создание и использование
// Создание канала
ch := make(chan int)
// Отправка (блокирующая)
ch <- 42
// Получение (блокирующая)
value := <-ch
// Проверка при получении
value, ok := <-ch
if !ok {
fmt.Println("Канал закрыт")
}
Простой пример
func main() {
ch := make(chan string)
// Горутина-отправитель
go func() {
ch <- "Привет из горутины!"
}()
// Получение в main
msg := <-ch
fmt.Println(msg)
}
Направленность каналов
// Только для отправки
func send(ch chan<- int) {
ch <- 42
}
// Только для получения
func receive(ch <-chan int) {
value := <-ch
fmt.Println(value)
}
func main() {
ch := make(chan int)
go send(ch)
receive(ch)
}
Буферизованные каналы
// Канал с буфером на 3 элемента
ch := make(chan int, 3)
// Можно отправить до 3 значений без блокировки
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // блокировка!
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
Разница в поведении
// Небуферизованный: синхронная передача
unbuffered := make(chan int)
go func() {
fmt.Println("Отправляю...")
unbuffered <- 1 // блокируется до получения
fmt.Println("Отправлено!")
}()
time.Sleep(2 * time.Second)
fmt.Println("Получено:", <-unbuffered)
// Буферизованный: асинхронная передача
buffered := make(chan int, 1)
buffered <- 1 // не блокируется
fmt.Println("Отправлено без блокировки")
fmt.Println("Получено:", <-buffered)
Закрытие каналов
ch := make(chan int, 3)
// Отправляем данные
ch <- 1
ch <- 2
ch <- 3
// Закрываем канал
close(ch)
// Можно читать из закрытого канала
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
fmt.Println(<-ch) // 0 (zero value)
// Проверка закрытия
if value, ok := <-ch; !ok {
fmt.Println("Канал закрыт")
}
// Range автоматически останавливается при закрытии
ch2 := make(chan int, 3)
ch2 <- 1
ch2 <- 2
ch2 <- 3
close(ch2)
for value := range ch2 {
fmt.Println(value) // 1, 2, 3
}
Select - мультиплексирование
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "из канала 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "из канала 2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Получено", msg1)
case msg2 := <-ch2:
fmt.Println("Получено", msg2)
}
}
}
Select с default
select {
case msg := <-ch:
fmt.Println("Получено:", msg)
default:
fmt.Println("Нет сообщений")
}
Select с таймаутом
select {
case msg := <-ch:
fmt.Println("Получено:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Таймаут!")
}
Идиоматичный пример: pipeline
Pipeline - каноничный приём в Go: каждая стадия - функция, которая получает входной канал и возвращает выходной. Хорошо показывает направленность каналов (<-chan/chan<-) и композицию через range по закрытому каналу.
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
for n := range square(generate(2, 3, 4)) {
fmt.Println(n) // 4, 9, 16
}
}
Каждая стадия отвечает за свой close - нижестоящая стадия завершит range сама, когда верхняя закроет канал.
Куда идти дальше с каналами
Остальные каналовые паттерны - worker pool, fan-in / fan-out, semaphore (ограничение конкурентности), graceful shutdown через signal.Notify - это уже не про синтаксис каналов, а про их композицию под конкретные задачи. Разобраны в отдельных треках:
- Паттерны: fan-in, fan-out, pipeline, worker pool
- Каналы: паттерны и подводные камни (done-channel, nil channel)
- errgroup и graceful shutdown
- Pub/Sub - для in-process подписки через каналы и для брокеров: Event-Driven · Pub/Sub
Типичные ошибки
Deadlock
// DEADLOCK!
func main() {
ch := make(chan int)
ch <- 42 // блокировка навсегда
}
// Решение
func main() {
ch := make(chan int)
go func() {
value := <-ch
fmt.Println(value)
}()
ch <- 42
}
Утечка горутин
// ПЛОХО: горутина утечет если не прочитать все значения
func leak() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i // может заблокироваться
}
}()
return ch
}
// ХОРОШО: используем контекст для отмены
func noLeak(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case <-ctx.Done():
return
}
}
}()
return ch
}
Best practices
<ComparisonTable data={{ headers: ["Практика", "Описание", "Пример"], rows: [ ["Кто закрывает", "Закрывает тот, кто отправляет", "Отправитель вызывает close()"], ["Nil каналы", "Операции с nil каналами блокируются навсегда", "Используйте для отключения case в select"], ["Буферизация", "Используйте для производительности, не для логики", "Буфер 1 для async отправки"], ["Направленность", "Используйте chan<- и <-chan в параметрах", "Защита от неправильного использования"], ["Select default", "Избегайте busy waiting", "Используйте блокирующий select"] ] }} />
Итоги
- Каналы - основа конкурентности в Go
- Буферизация влияет на блокировку
- Select для работы с несколькими каналами
- Закрывает канал отправитель
- Думайте о жизненном цикле горутин
В следующем уроке создадим веб-сервер на Go!
Типичная ошибка
Отправлять в канал без получателя - программа зависнет (deadlock). Всегда думай о том, кто и когда прочитает из канала.