Как каналы используются для сигнализации между горутинами в Go?

Ответ

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

Для сигнализации обычно используют каналы типа chan struct{}. Пустая структура struct{} не занимает памяти, что делает ее идеальной для передачи сигналов, когда важен сам факт события, а не его содержимое.

Пример использования для сигнализации о завершении работы:

package main

import (
    "fmt"
    "time"
)

func worker(done chan struct{}) {
    fmt.Println("Рабочая горутина: Начинаю работу...")
    time.Sleep(2 * time.Second) // Имитация выполнения задачи
    fmt.Println("Рабочая горутина: Работа завершена.")
    close(done) // Отправляем сигнал о завершении, закрывая канал
}

func main() {
    // Создаем небуферизованный канал для сигнала
    done := make(chan struct{})

    // Запускаем рабочую горутину
    go worker(done)

    fmt.Println("Главная горутина: Ожидаю сигнала о завершении...")
    <-done // Блокируемся здесь до тех пор, пока канал 'done' не будет закрыт
    fmt.Println("Главная горутина: Получен сигнал о завершении. Продолжаю работу.")
}

Ключевые моменты:

  1. Тип канала chan struct{}: Используется для минимизации потребления памяти, так как struct{} не занимает места.
  2. Закрытие канала (close(done)): Это широковещательный сигнал. Все горутины, которые в данный момент читают из этого канала или попытаются прочитать из него после закрытия, немедленно получат нулевое значение (для struct{} это будет struct{}{}) и ok будет false (если используется v, ok := <-ch). Это позволяет уведомить множество слушателей одновременно.
  3. Чтение из канала (<-done): Блокирует горутину до тех пор, пока канал не будет закрыт или в него не будет отправлено значение (если канал буферизован и в него что-то отправлено).
  4. Буферизированные каналы для сигнализации: Могут быть использованы, например, make(chan struct{}, 1). Отправка сигнала в такой канал (done <- struct{}{}) не будет блокировать отправителя, если буфер не полон. Это полезно, когда отправитель не хочет ждать, пока получатель будет готов принять сигнал, или когда сигнал может быть пропущен, если получатель не слушает немедленно.