Каналы в Go на простых примерах: пошаговая инструкция
Каналы — главный механизм общения между горутинами в Go. Разбираем небуферизованные и буферизованные каналы, select, однонаправленные типы и паттерн fan-out/fan-in на рабочих примерах, которые можно скопировать и запустить.

Что такое канал и зачем он нужен
Горутины без каналов — как потоки без очередей: работают параллельно, но не могут безопасно передать друг другу результат. Канал в Go — это типизированная труба: одна горутина кладёт значение, другая забирает. Никаких мьютексов, никаких гонок — компилятор и рантайм берут синхронизацию на себя.
Ниже — пошаговый разбор от простейшего канала до паттерна fan-out/fan-in. Все примеры написаны для Go 1.22+, запускаются как есть через go run main.go.
Что нужно до начала
- Go 1.22 или новее — проверьте командой
go version. - Любой текстовый редактор или IDE с поддержкой Go (VS Code с расширением Go, GoLand).
- Терминал, в котором доступна команда
go run.
Шаг 1. Небуферизованный канал
Небуферизованный канал блокирует отправителя, пока получатель не прочитает значение, и наоборот. Это самый строгий вид синхронизации: обе стороны встречаются в одной точке.
package main
import "fmt"
func main() {
// Создаём канал для строк
messages := make(chan string)
// Горутина-отправитель
go func() {
messages <- "привет из горутины"
}()
// Основная горутина ждёт и читает
msg := <-messages
fmt.Println(msg)
}
Проверка: в терминале появится строка привет из горутины. Если убрать чтение из канала, программа зависнет — рантайм обнаружит deadlock и завершит её с ошибкой fatal error: all goroutines are asleep - deadlock!.
Шаг 2. Буферизованный канал
Буферизованный канал позволяет отправить до N значений без блокировки. Отправитель заблокируется, только когда буфер полон; получатель — когда буфер пуст.
package main
import "fmt"
func main() {
// Буфер на 2 элемента
tasks := make(chan int, 2)
tasks <- 10
tasks <- 20
// tasks <- 30 // раскомментируйте — будет deadlock, буфер полон
fmt.Println(<-tasks) // 10
fmt.Println(<-tasks) // 20
}
Проверка: вывод — 10 и 20. Попробуйте раскомментировать третью отправку и увидите deadlock.
Когда выбирать буферизованный канал
Буфер полезен, когда производитель и потребитель работают с разной скоростью и вы хотите сгладить всплески, а не ждать каждую передачу. Типичный случай — воркер-пул, где задач больше, чем воркеров, и очередь на несколько элементов снижает простой.
Шаг 3. Закрытие канала и range
Отправитель закрывает канал, чтобы сообщить: данных больше не будет. Получатель может перебирать значения через range — цикл остановится сам, когда канал закрыт и пуст.
package main
import "fmt"
// generateNumbers отправляет числа и закрывает канал
func generateNumbers(count int, out chan<- int) {
for i := 1; i <= count; i++ {
out <- i
}
close(out)
}
func main() {
numbers := make(chan int, 5)
go generateNumbers(5, numbers)
// range читает, пока канал не закрыт
for n := range numbers {
fmt.Println(n)
}
}
Проверка: числа от 1 до 5, каждое на отдельной строке.
Обратите внимание на тип параметра out chan<- int — это однонаправленный канал только для записи. Компилятор не даст из него читать внутри функции. Симметрично, <-chan int — канал только для чтения. Однонаправленные типы делают контракт функции явным и ловят ошибки на этапе компиляции.
Закрывать канал должен только отправитель. Попытка записи в закрытый канал вызывает панику — и это не предупреждение, а аварийное завершение программы.
Шаг 4. select — мультиплексирование каналов
Конструкция select ждёт сразу несколько каналов и выполняет ветку того, который готов первым. Если готовы оба — выбор случайный. Это аналог switch, но для каналов.
package main
import (
"fmt"
"time"
)
func main() {
fast := make(chan string)
slow := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
fast <- "быстрый ответ"
}()
go func() {
time.Sleep(500 * time.Millisecond)
slow <- "медленный ответ"
}()
// Ждём первый готовый канал
select {
case msg := <-fast:
fmt.Println("получено:", msg)
case msg := <-slow:
fmt.Println("получено:", msg)
case <-time.After(1 * time.Second):
fmt.Println("таймаут")
}
}
Проверка: вывод — получено: быстрый ответ, потому что горутина с 100 мс сна завершается раньше.
Ветка time.After — стандартный приём для таймаутов. Без неё select будет ждать бесконечно, если ни один канал не ответит.
Шаг 5. Fan-out / fan-in
Fan-out — раздать задачи нескольким горутинам. Fan-in — собрать результаты в один канал. Это базовый паттерн параллельной обработки.
package main
import (
"fmt"
"sync"
)
// worker читает задачи, возводит в квадрат и отправляет результат
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
fmt.Printf("воркер %d обрабатывает %d\n", id, j)
results <- j * j
}
}
func main() {
const numWorkers = 3
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
// Запускаем воркеров (fan-out)
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Отправляем задачи
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// Закрываем results после завершения всех воркеров
go func() {
wg.Wait()
close(results)
}()
// Собираем результаты (fan-in)
for r := range results {
fmt.Println("результат:", r)
}
}
Проверка: девять строк с квадратами чисел от 1 до 9. Порядок может отличаться между запусками — горутины работают параллельно.
Сравнение видов каналов
| Свойство | Небуферизованный | Буферизованный |
|---|---|---|
| Создание | make(chan T) | make(chan T, N) |
| Блокировка при отправке | Пока кто-то не прочитает | Пока буфер не заполнен |
| Блокировка при чтении | Пока кто-то не отправит | Пока буфер пуст |
| Гарантия синхронной передачи | Да — рандеву | Нет — значение может ждать в буфере |
| Типичное применение | Сигналы, строгая синхронизация | Очереди задач, сглаживание всплесков |
Если не получилось
fatal error: all goroutines are asleep - deadlock!— вы отправляете в канал, но никто из него не читает (или наоборот). Проверьте, что на каждую отправку есть получатель, или используйте буферизованный канал.panic: send on closed channel— попытка записи в закрытый канал. Закрывайте канал только на стороне отправителя и только один раз.panic: close of closed channel— двойное закрытие. Если несколько горутин пишут в один канал, используйтеsync.WaitGroupи закрывайте канал в отдельной горутине послеwg.Wait(), как в примере fan-out/fan-in.- Программа завершается до вывода — главная горутина (
main) не ждёт дочерние. Добавьте чтение из канала илиsync.WaitGroup. - Утечка горутин — горутина навсегда заблокирована на канале, в который никто не пишет и который не закрыт. Инструмент
runtime.NumGoroutine()покажет текущее число горутин; если оно растёт — ищите незакрытые каналы.
* Горутина — легковесный поток исполнения, управляемый рантаймом Go, а не операционной системой. Стартовый размер стека — около 2–8 КБ (зависит от версии), стек растёт динамически. Запускается оператором go f().
Что дальше
Каналы — фундамент, но не единственный инструмент конкурентности в Go. Следующий шаг — разобраться с пакетом context: он позволяет отменять цепочки горутин и передавать дедлайны, что критично для любого HTTP-сервера или CLI-инструмента с таймаутами. Полезно также посмотреть на пакет sync — sync.Mutex и sync.Once бывают проще каналов, когда нужно просто защитить переменную, а не организовать поток данных.
Для отладки конкурентного кода используйте флаг -race: команда go run -race main.go включает детектор гонок и покажет, если две горутины одновременно обращаются к одной переменной без синхронизации.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Effective Go — Concurrency (официальная документация), A Tour of Go — Channels
Частые вопросы
Чем отличается небуферизованный канал от буферизованного?
Что будет, если записать в закрытый канал?
Может ли чтение из канала вернуть нулевое значение?
Как избежать утечки горутин?
Работают ли примеры из статьи на Go 1.18 или более ранних версиях?
Когда лучше использовать мьютекс, а не канал?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.