Содержание

Каналы в Go

Каналы в Go нужны для обмена данными между goroutine. Если goroutine - это независимые работники, то channel - это конвейер или очередь, по которой один работник передает результат другому.

Важно не воспринимать канал как "магическую безопасную переменную". Канал - это инструмент координации. Он помогает ответить на вопросы: кто отправляет данные, кто принимает, когда работа закончилась и что делать, если несколько событий происходят одновременно.

Зачем нужны каналы

Представьте кухню. Один человек нарезает овощи, другой готовит суп. Между ними стоит миска. Первый кладет туда нарезанные овощи, второй забирает. Миска не готовит суп сама, но помогает организовать передачу работы.

Канал делает похожую вещь:

jobs := make(chan string)

Одна goroutine может отправить значение:

jobs <- "file.txt"

Другая goroutine может получить:

job := <-jobs

Небуферизованный канал

Если размер буфера не указан, канал небуферизованный:

ch := make(chan int)

Отправка в такой канал блокируется, пока кто-то не начнет принимать значение. Получение блокируется, пока кто-то не отправит значение.

func main() { ch := make(chan int) go func() { ch <- 42 }() value := <-ch fmt.Println(value) }

Это не просто передача данных, но и синхронизация. Отправитель и получатель встречаются в одной точке.

Буферизованный канал

Буферизованный канал может хранить несколько значений:

ch := make(chan int, 2) ch <- 1 ch <- 2

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

Буфер полезен, когда отправитель и получатель работают с разной скоростью. Но слишком большой буфер может спрятать проблему: producer создает задачи быстрее, чем consumer успевает их обрабатывать.

close

close сообщает получателям, что новых значений больше не будет:

close(ch)

Закрывать канал должен отправитель, а не получатель. Получатель не знает, будут ли еще значения от других отправителей.

Чтение из закрытого канала возвращает нулевое значение и ok=false:

value, ok := <-ch if !ok { fmt.Println("channel closed") }

Отправка в закрытый канал вызывает panic. Поэтому не закрывайте канал, если не контролируете всех отправителей.

range по каналу

Если канал закрывается после отправки всех значений, его удобно читать через range:

jobs := make(chan string) go func() { defer close(jobs) jobs <- "a.txt" jobs <- "b.txt" }() for job := range jobs { fmt.Println("process", job) }

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

Channel directions

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

func producer(out chan<- int) { out <- 1 } func consumer(in <-chan int) { value := <-in fmt.Println(value) }

chan<- int означает "только отправлять". <-chan int означает "только получать". Это делает контракт функции яснее и помогает компилятору ловить ошибки.

select

select ждет несколько channel operations:

select { case msg := <-messages: fmt.Println("message:", msg) case err := <-errors: fmt.Println("error:", err) case <-time.After(5 * time.Second): fmt.Println("timeout") }

Он похож на switch, но для каналов. Выполнится тот case, который готов первым.

select часто используют для:

  • timeout;
  • отмены через context;
  • ожидания результата или ошибки;
  • чтения из нескольких источников.

Context и отмена

Каналы хорошо сочетаются с context:

func worker(ctx context.Context, jobs <-chan Job) { for { select { case <-ctx.Done(): return case job, ok := <-jobs: if !ok { return } process(job) } } }

Так worker завершится и при закрытии jobs, и при отмене context.

Deadlock

Deadlock возникает, когда goroutine ждут друг друга, и никто не может продолжить.

Пример:

func main() { ch := make(chan int) ch <- 1 fmt.Println(<-ch) }

Отправка в небуферизованный канал ждет получателя, но получатель находится ниже в той же goroutine. Программа зависнет.

Исправление:

go func() { ch <- 1 }() fmt.Println(<-ch)

или буфер:

ch := make(chan int, 1) ch <- 1 fmt.Println(<-ch)

Канал или mutex

Канал не всегда лучше mutex. Выбирайте инструмент по задаче:

ЗадачаИнструмент
Передать работу worker-амchannel
Дождаться результатаchannel
Ограничить количество задачbuffered channel
Защитить map от одновременной записиsync.Mutex
Посчитать завершение группы goroutinesync.WaitGroup

Хорошее правило: используйте канал для передачи владения данными или событий. Используйте mutex для защиты общего состояния.

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

  1. Отправлять в канал, у которого нет получателя.
  2. Читать из канала, в который никто не отправляет.
  3. Закрывать канал со стороны получателя.
  4. Отправлять в закрытый канал.
  5. Использовать channel там, где достаточно sync.Mutex.
  6. Делать огромный буфер, чтобы скрыть медленного consumer.
  7. Забывать об отмене worker-ов.

Чеклист

  1. Понятно, кто отправляет данные в канал.
  2. Понятно, кто закрывает канал.
  3. Получатели корректно обрабатывают закрытие.
  4. Буфер канала выбран осознанно.
  5. Для долгих worker-ов есть отмена через context.
  6. select используется там, где нужно ждать несколько событий.
  7. Канал не заменяет mutex без причины.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи