Модуль
1Горутины: основы2Каналы← вы здесь3select и таймауты4sync: Mutex, Once, atomic
Урок 2~12 минут

Каналы

Каналы: общение через передачу данных

Философия Go: «не общайся через разделяемую память — разделяй память через общение». Каналы — это типизированные трубы для передачи данных между горутинами.

go
ch := make(chan int)      // небуферизованный канал int
ch <- 42                  // отправка (блокирует до получателя)
val := <-ch               // получение (блокирует до отправителя)
val, ok := <-ch           // получение с проверкой: ok=false если канал закрыт

Тип канала: chan T. Оператор <- указывает направление потока данных.


Небуферизованные каналы: точка синхронизации

Небуферизованный канал (make(chan T)) — это рандеву: оба участника должны быть готовы одновременно:

go
package main
 
import "fmt"
 
func sum(s []int, ch chan int) {
    total := 0
    for _, v := range s {
        total += v
    }
    ch <- total  // отправляем результат
}
 
func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 
    ch := make(chan int)
    go sum(nums[:5], ch)   // считает 1+2+3+4+5 = 15
    go sum(nums[5:], ch)   // считает 6+7+8+9+10 = 40
 
    a, b := <-ch, <-ch     // получаем оба результата
    fmt.Println(a + b)     // 55
}

main блокируется на <-ch пока горутина не отправит результат — это встроенная синхронизация без WaitGroup.


Буферизованные каналы: асинхронная очередь

Буфер позволяет отправить несколько значений без получателя:

go
ch := make(chan int, 3)  // буфер на 3 элемента
 
ch <- 1  // не блокирует
ch <- 2  // не блокирует
ch <- 3  // не блокирует
ch <- 4  // БЛОКИРУЕТ — буфер полон
 
fmt.Println(len(ch))  // 3 — элементов в буфере
fmt.Println(cap(ch))  // 3 — ёмкость буфера

Буферизованный канал — это FIFO очередь. Отправитель блокируется только когда буфер заполнен, получатель — только когда буфер пуст.

go
// Типичный use case: ограничитель конкурентности
sem := make(chan struct{}, 5)  // не более 5 горутин одновременно
 
for _, url := range urls {
    sem <- struct{}{}  // занять слот
    go func(u string) {
        defer func() { <-sem }()  // освободить слот
        fetch(u)
    }(url)
}

Закрытие канала и range

Закрытый канал сигнализирует получателям: «данных больше не будет»:

go
func generate(n int) chan int {
    ch := make(chan int)
    go func() {
        for i := 0; i < n; i++ {
            ch <- i
        }
        close(ch)  // закрываем — получатели узнают об окончании
    }()
    return ch
}
 
func main() {
    for v := range generate(5) {  // range завершится после close
        fmt.Println(v)  // 0 1 2 3 4
    }
}

Правила закрытия:

  • Закрывает только отправитель, никогда получатель
  • Закрывать можно только один раз (повторное close → panic)
  • Отправка в закрытый канал → panic
  • Чтение из закрытого канала → оставшиеся данные, потом нулевое значение + ok=false
go
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
 
v1, ok1 := <-ch  // 10, true
v2, ok2 := <-ch  // 20, true
v3, ok3 := <-ch  // 0, false — канал закрыт и пуст

Направленные каналы

Можно ограничить канал только на отправку или только на получение:

go
chan<- int   // только отправка (write-only)
<-chan int   // только получение (read-only)
chan int     // двунаправленный

Это позволяет выражать намерения в сигнатурах функций и ловить ошибки на этапе компиляции:

go
func producer(out chan<- int) {
    for i := 0; i < 5; i++ {
        out <- i
    }
    close(out)
    // <-out  // ошибка компиляции: receive from send-only channel
}
 
func consumer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
    // in <- 1  // ошибка компиляции: send to receive-only channel
}
 
func main() {
    ch := make(chan int, 5)
    go producer(ch)  // chan int неявно конвертируется в chan<- int
    consumer(ch)     // и в <-chan int
}

Паттерн Pipeline

Каналы отлично подходят для построения конвейеров обработки данных:

go
// Этап 1: генератор чисел
func numbers(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}
 
// Этап 2: возводим в квадрат
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}
 
func main() {
    // Соединяем этапы
    nums := numbers(2, 3, 4, 5)
    squares := square(nums)
 
    for v := range squares {
        fmt.Println(v)  // 4 9 16 25
    }
}

Каждый этап — независимая горутина. Данные текут по каналам. Это идиоматичный Go.


Паттерн Fan-out / Fan-in

Fan-out — один источник, много потребителей:

go
func fanOut(in <-chan int, n int) []<-chan int {
    outs := make([]<-chan int, n)
    for i := range outs {
        outs[i] = square(in)  // несколько worker'ов на один вход
    }
    return outs
}

Fan-in — много источников, один потребитель:

go
func merge(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int)
 
    forward := func(ch <-chan int) {
        defer wg.Done()
        for v := range ch {
            merged <- v
        }
    }
 
    wg.Add(len(channels))
    for _, ch := range channels {
        go forward(ch)
    }
 
    go func() {
        wg.Wait()
        close(merged)
    }()
 
    return merged
}

Эти паттерны — основа построения эффективных конкурентных систем в Go.

Небуферизованный канал — точка синхронизации: отправитель ждёт получателя и наоборот. Буферизованный — очередь: отправитель блокируется только когда буфер полон.
Визуализация
goroutine sender
ch <- 1
chan int (unbuffered)
<- значение идёт через канал ->
goroutine receiver
v := <-ch
Отправлено: 0Получено: 0
Лог
// Нажмите "Отправить", затем "Получить"
// Небуферизованный канал ch := make(chan int) // goroutine отправитель — блокируется до получателя go func() { ch <- 42 // BLOCKED пока нет получателя }() // goroutine получатель — разблокирует отправителя v := <-ch fmt.Println(v) // 42
🎯
Миссия 1 из 4
Как создать небуферизованный канал целых чисел?