---
title: "Каналы"
description: "Горутина скачала страницу и должна отдать её парсеру, который работает в другой горутине. Можно положить страницу в общую переменную, защитить её мьютексом и как-то сообщить…"
canonical: https://graphlms.ru/go/book/channels
course: "Конкурентность Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Каналы

> Источник: https://graphlms.ru/go/book/channels — курс «Конкурентность Go», GraphLMS.

## О чём эта глава

Горутина скачала страницу и должна отдать её парсеру, который работает в другой
горутине. Можно положить страницу в общую переменную, защитить её мьютексом и как-то
сообщить парсеру, что пора читать. А можно отправить страницу в канал: парсер получит
её ровно тогда, когда она готова, и после этого скачивающая горутина к ней больше не
прикасается.

Канал — типизированная очередь между горутинами, у которой каждая операция ещё и
синхронизирует. Отправка happens-before соответствующего приёма (см.
[модель памяти](/go/book/memory-model)), поэтому всё, что отправитель записал до
отправки, получатель гарантированно видит после приёма. Отсюда девиз Go: *«Don't
communicate by sharing memory; share memory by communicating»* — не общайтесь через
общую память, а передавайте память через общение.

В главе четыре темы, на которых держится почти весь конкурентный Go: буфер и его
отсутствие, закрытие канала как сигнал всем получателям, `nil`-канал и направления
каналов в сигнатурах.

## Перед чтением

Нужны `go`, `make`, срезы, циклы и функции с несколькими результатами. Базовый
синтаксис канала уже был во [вводной главе](/go-basics/book/goroutines-intro):

```go
ch := make(chan int)        // небуферизованный канал
ch := make(chan int, 100)   // канал с буфером на 100 элементов
ch <- 42                    // отправка
v := <-ch                   // приём
v, ok := <-ch               // ok == false, если канал закрыт и пуст
```

Читая главу, держите в голове три вопроса к каждому каналу: когда операция с ним
ждёт, как получатель узнаёт, что данных больше не будет, и кто имеет право канал
закрыть. Создать канал и отвечать за его закрытие — разные роли: функция может
создать канал и отдать право закрытия горутине-отправителю.

Пример с выключением веток через `nil` использует `select` из следующей главы. Его
можно пропустить и вернуться к нему вместе с [select](/go/book/select).

## Буфер и его отсутствие

От размера буфера зависит, когда именно горутина остановится на операции с каналом.

**Небуферизованный канал** (`make(chan T)`) — точка встречи. Отправка не кладёт значение
в очередь: она ждёт, пока кто-то начнёт принимать. Приём ждёт, пока кто-то отправит.
Значение переходит из рук в руки в момент встречи. Поэтому после успешной отправки
отправитель знает, что получатель значение уже забрал.

**Буферизованный канал** (`make(chan T, n)`) разрешает отправить до `n` значений, не
дожидаясь получателя. Отправка ждёт только при полном буфере, приём — только при
пустом. Буфер сглаживает рывки: если отправитель на мгновение обогнал получателя,
значения подождут в буфере.

```go
ch := make(chan int, 2)
ch <- 1   // сразу: в буфере 1 из 2
ch <- 2   // сразу: в буфере 2 из 2
ch <- 3   // ждёт: буфер полон, а получателя нет
```

Если такой код стоит в `main` и других горутин нет, третья отправка не дождётся
никогда. Рантайм замечает, что все горутины спят и разбудить их некому, и аварийно
завершает программу:

```text
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/demo/main.go:7 +0x38
exit status 2
```

Строка `goroutine 1 [chan send]` говорит, где и на какой операции застряла горутина.
Это сообщение вы будете видеть часто; читайте его снизу вверх: файл и строка, затем
операция в квадратных скобках.

Пример с буфером, которого хватает на все значения:

```go play
package main

import "fmt"

func main() {
	ch := make(chan int, 3)
	for i := 1; i <= 3; i++ {
		ch <- i * i // не ждём: место в буфере есть
		fmt.Println("отправил", i*i, "len/cap:", len(ch), "/", cap(ch))
	}
	close(ch)
	for v := range ch {
		fmt.Println("прочитал", v)
	}
}
```

```text
отправил 1 len/cap: 1 / 3
отправил 4 len/cap: 2 / 3
отправил 9 len/cap: 3 / 3
прочитал 1
прочитал 4
прочитал 9
```

`cap(ch)` — размер буфера, `len(ch)` — сколько значений лежит в нём сейчас. Для
отладки удобно, но принимать решения по `len(ch)` в конкурентном коде нельзя: к
следующей строке значение уже может измениться.

Буфер не ускоряет обработку и не отменяет того, что кто-то должен значения прочитать.
Его размер выбирают осознанно. Буфер 1 часто ставят, чтобы горутина-отправитель могла
отдать единственный результат и завершиться, даже если получатель уже ушёл по
таймауту; этот приём подробно разобран в главе
[«Утечки горутин и гонки»](/go/book/leaks-and-races). Большой буфер, наоборот,
настораживает: он прячет ситуацию «получатель не успевает». Отправитель узнаёт о
проблеме не сразу, а когда очередь уже раздулась.

## Закрытие как сигнал всем

`close(ch)` означает «данных больше не будет». Из закрытого канала приём никогда не
ждёт: сначала он отдаёт значения, оставшиеся в буфере (с `ok == true`), а когда буфер
опустел — нулевое значение типа и `ok == false`.

```go
done := make(chan struct{})
close(done)
<-done   // не ждёт
<-done   // и снова не ждёт: закрытие видно всем и навсегда
```

Отсюда главное свойство: одно закрытие будит **всех** горутин, ждущих приёма. Отправка
будит одного получателя, а `close` — сколько угодно. На этом построены сигнальные
каналы `done` и весь пакет [context](/go/book/context). Для чистого сигнала берут тип
`chan struct{}`: значение не несёт данных и не занимает памяти.

Три воркера ждут одного сигнала старта:

```go play
package main

import (
	"fmt"
	"sync"
)

func main() {
	start := make(chan struct{})
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			<-start // ждём общего сигнала
			fmt.Println("воркер", id, "поехал")
		}(i)
	}
	close(start) // один close будит всех
	wg.Wait()
	fmt.Println("все воркеры стартовали")
}
```

Все три строки «воркер N поехал» будут напечатаны, но в каком порядке — не определено.
`все воркеры стартовали` всегда последняя: её печатает `main` после `wg.Wait()`.

Нарушение правил закрытия вызывает панику:

- закрыть уже закрытый канал — `panic: close of closed channel`;
- отправить в закрытый канал — `panic: send on closed channel`;
- закрыть `nil`-канал — `panic: close of nil channel`.

Из этого правило: **канал закрывает отправитель, и только один**. Получатель не знает,
закончили ли отправители, и закрывать канал не должен. Если отправителей несколько,
закрытие поручают координатору: он ждёт, пока все отправители закончат (обычно через
`WaitGroup`), и закрывает канал сам. Этот приём называется fan-in и подробно разобран
в главе [«Паттерны»](/go/book/patterns). `sync.Once` вокруг `close` спасает только от
повторного закрытия, но не от отправки в уже закрытый канал.

`for range` по каналу читает значения, пока канал не закрыт и не опустошён, а потом
выходит из цикла. Это самый частый способ дочитать отправителя до конца:

```go play
package main

import "fmt"

func main() {
	nums := make(chan int)
	go func() {
		for i := 1; i <= 4; i++ {
			nums <- i
		}
		close(nums) // без этого range ждал бы вечно
	}()

	sum := 0
	for v := range nums { // выходит, когда канал закрыт и пуст
		sum += v
	}
	fmt.Println("сумма:", sum)
}
```

```text
сумма: 10
```

## nil-каналы

Отправка в `nil`-канал и приём из него ждут вечно:

```go
var ch chan int   // nil: канал объявлен, но не создан через make
<-ch              // ждёт вечно
ch <- 1           // ждёт вечно
```

Сама по себе такая операция бесполезна, но внутри [select](/go/book/select) это
выключатель. Ветка с `nil`-каналом никогда не будет выбрана. Присвоив переменной
канала `nil`, вы убираете ветку из рассмотрения, не меняя код `select`.

Типичный случай: читаем два канала, и один из них закрылся. Приём из закрытого канала
не ждёт, поэтому `select` выбирал бы эту ветку снова и снова, получая нули. Если
обнулить переменную закрытого канала, `select` будет ждать только второй:

```go play
package main

import "fmt"

func main() {
	a := make(chan int, 2)
	b := make(chan int, 2)
	a <- 1
	a <- 2
	close(a)
	b <- 10
	b <- 20
	close(b)

	sum := 0
	for a != nil || b != nil {
		select {
		case v, ok := <-a:
			if !ok {
				a = nil // ветка a больше не будет выбрана
				continue
			}
			sum += v
		case v, ok := <-b:
			if !ok {
				b = nil
				continue
			}
			sum += v
		}
	}
	fmt.Println("слили оба канала, сумма:", sum)
}
```

```text
слили оба канала, сумма: 33
```

Цикл продолжается, пока жив хотя бы один источник. Когда оба канала обнулены, условие
`a != nil || b != nil` ложно и цикл заканчивается.

## Направления каналов

В сигнатуре функции канал можно сузить до «только отправка» или «только приём»:

```go
func produce(out chan<- int) { out <- 1 } // chan<- int: только отправлять
func consume(in <-chan int)  { <-in }      // <-chan int: только принимать
```

Стрелка показывает, куда движутся данные относительно канала. Обычный `chan int`
автоматически приводится к `chan<- int` или `<-chan int` при передаче в функцию, а
обратно — нет. Компилятор не даст функции-потребителю отправить в канал или закрыть
его: закрыть можно только двунаправленный канал или канал «только для отправки». Так
роли видны прямо из сигнатуры.

```go play
package main

import "fmt"

func produce(out chan<- int) {
	for i := 1; i <= 3; i++ {
		out <- i
	}
	close(out)
}

func sum(in <-chan int) int {
	total := 0
	for v := range in {
		total += v
	}
	return total
}

func main() {
	ch := make(chan int)
	go produce(ch) // chan int подходит как chan<- int
	fmt.Println("итог:", sum(ch))
}
```

```text
итог: 6
```

## Частые ошибки

**Отправка в закрытый канал.** Чаще всего бывает, когда отправителей несколько и один
из них закрыл канал, пока другие ещё работают. Решение — один владелец закрытия,
который точно знает, что все отправители закончили.

**Горутина зависла на отправке.** Получатель ушёл, а горутина ждёт на `ch <- v` вечно и
держит свой стек и всё, на что ссылается:

```go
func leak() chan int {
	ch := make(chan int)
	go func() { ch <- expensive() }() // если результат не прочитают, горутина не завершится
	return ch
}
```

Варианты исправления: буфер 1 (единственный результат ляжет в буфер), сигнал отмены в
`select` рядом с отправкой или гарантия, что получатель всегда дочитывает канал.

**Закрытие со стороны получателя.** Получатель закрывает канал «на всякий случай», а
живой отправитель на следующей отправке паникует. Получатель канал не закрывает.

**`for range` по каналу, который никто не закроет.** Цикл будет ждать следующего значения
вечно. Если канал читают через `range`, закрытие — обязательная часть протокола, и в
коде должно быть видно, кто его делает.

## Мысленная модель

У каждого канала три состояния, и каждое отвечает на свой вопрос:

- **Открытый канал** передаёт значения, а вместе с ними — видимость всего, что
  отправитель записал до отправки. Владение данными канал при этом не передаёт
  автоматически: если вы отправили указатель, срез или карту, договоритесь, что
  отправитель после отправки их не меняет.
- **Закрытый канал** говорит «событий больше не будет» всем получателям сразу и
  навсегда.
- **`nil`-канал** никогда не срабатывает; в `select` это выключенная ветка.

Почти любой дедлок, утечка или паника на канале объясняется одним из этих трёх
состояний.

## Попробуйте сами

Создайте канал `int` с буфером 2, отправьте `10` и `20`, закройте его. До запуска
запишите результаты трёх подряд приёмов `v, ok := <-ch`. Потом проверьте кодом.
Понадобится ли горутина для двух первых отправок?

### Проверка

```go
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
for i := 0; i < 3; i++ {
    v, ok := <-ch
    fmt.Println(v, ok)
}
```

```text
10 true
20 true
0 false
```

Буфера хватает на обе отправки, поэтому горутина не нужна. Закрытие не удаляет
значения, уже лежащие в буфере. Все следующие приёмы тоже вернут `0 false`, а
отправка после `close` вызовет панику.

## Что дальше

Главный навык для задач [блока 1](/go/topics/1) и [блока 3](/go/topics/3) — для каждого канала уметь
ответить, **кто его закрывает и кто гарантированно дочитает**. Если на оба вопроса
есть ответ, конкурентный код почти наверняка завершится корректно.

Следующая глава, [select](/go/book/select), учит ждать сразу несколько каналов и
добавлять к ожиданию таймаут и отмену. Дальше каналы встретятся в
[context](/go/book/context) (сигнал отмены — это закрытый канал) и в
[паттернах](/go/book/patterns), где из них собирают конвейеры и пулы.
