Опишите основные способы организации взаимодействия и обмена данными между горутинами в Go.

Ответ

В Go основной и идиоматичный способ взаимодействия между горутинами — это каналы (channels). Они реализуют ключевой принцип языка: "Не обменивайтесь данными через общую память; вместо этого, делитесь памятью путем обмена данными".

1. Каналы (Channels)

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

  • Небуферизированные каналы (make(chan T)): Отправка блокируется до тех пор, пока другая горутина не будет готова принять данные. Это мощный механизм для синхронизации.
  • Буферизированные каналы (make(chan T, N)): Отправка блокируется только когда буфер полон. Это позволяет горутинам-отправителям и получателям работать с разной скоростью (асинхронно).

Пример с sync.WaitGroup для корректного завершения:

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {
        fmt.Printf("Worker %d started job %dn", id, j)
        time.Sleep(time.Second)
        fmt.Printf("Worker %d finished job %dn", id, j)
    }
}

func main() {
    jobs := make(chan int, 5)
    var wg sync.WaitGroup

    // Запускаем 3 воркера
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, &wg)
    }

    // Отправляем 5 заданий
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // Закрываем канал, чтобы воркеры завершили работу

    wg.Wait() // Ждем завершения всех воркеров
}

2. Оператор select

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

select {
case msg1 := <-ch1:
    fmt.Println("received", msg1)
case ch2 <- msg2:
    fmt.Println("sent", msg2)
default:
    // Выполняется, если ни один канал не готов
    // (делает select неблокирующим)
}

3. Примитивы синхронизации из пакета sync

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

  • sync.Mutex и sync.RWMutex: Используются для защиты критических секций кода, которые обращаются к общим данным. RWMutex позволяет множественные чтения или одну запись.
  • sync.WaitGroup: Используется для ожидания завершения работы группы горутин.
  • sync.Once: Гарантирует, что определенный участок кода выполнится только один раз.
  • sync.Cond: Более сложный механизм для горутин, ожидающих наступления определенного условия.