Каналы - связь между горутинами

"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 - это уже не про синтаксис каналов, а про их композицию под конкретные задачи. Разобраны в отдельных треках:

Типичные ошибки

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). Всегда думай о том, кто и когда прочитает из канала.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.