В каких случаях в Go целесообразно использовать конкурентность (горутины и каналы)?

Ответ

Конкурентность в Go — это мощный инструмент, который следует использовать для решения определенных классов задач, где он дает наибольший выигрыш.

Основные случаи применения:

  1. Операции, связанные с вводом-выводом (I/O-bound задачи):

    • Описание: Это задачи, где программа большую часть времени ожидает ответа от внешнего ресурса. Горутины позволяют эффективно использовать процессорное время, пока одна из них заблокирована ожиданием.
    • Примеры:
      • Сетевые запросы (HTTP API, gRPC).
      • Работа с базами данных.
      • Чтение/запись файлов на диск.
      • Взаимодействие с брокерами сообщений (Kafka, RabbitMQ).
  2. Распараллеливание вычислений (CPU-bound задачи):

    • Описание: Это задачи, требующие интенсивных вычислений. Для получения реального ускорения необходимо наличие нескольких ядер процессора (GOMAXPROCS > 1). Горутины позволяют разбить одну большую задачу на несколько меньших и выполнить их параллельно.
    • Примеры:
      • Обработка изображений или видео.
      • Сложные математические расчеты.
      • Криптографические операции.
  3. Обработка событий и асинхронных потоков данных:

    • Описание: Когда приложению нужно одновременно реагировать на множество независимых событий.
    • Примеры:
      • Веб-сервер, обрабатывающий тысячи одновременных подключений.
      • Приложения, работающие с WebSocket.
      • Потребители (consumers) из очередей сообщений.

Пример: конкурентная загрузка сайтов

func fetchURLs(urls []string) {
    var wg sync.WaitGroup // Ожидаем завершения всех горутин
    ch := make(chan string)   // Канал для результатов

    for _, url := range urls {
        wg.Add(1) // Увеличиваем счетчик горутин
        go func(u string) {
            defer wg.Done() // Уменьшаем счетчик по завершении
            resp, err := http.Get(u)
            if err != nil {
                ch <- fmt.Sprintf("Ошибка при загрузке %s: %v", u, err)
                return
            }
            defer resp.Body.Close()
            ch <- fmt.Sprintf("Сайт %s доступен, статус: %s", u, resp.Status)
        }(url)
    }

    // Горутина для закрытия канала, когда все загрузки завершатся
    go func() {
        wg.Wait()
        close(ch)
    }()

    // Читаем все результаты из канала
    for result := range ch {
        fmt.Println(result)
    }
}

Когда не стоит использовать:

  • Для простых, последовательных операций, где накладные расходы на создание горутин и синхронизацию могут превысить выгоду.
  • Без понимания потенциальных проблем: состояний гонки (race conditions), взаимных блокировок (deadlocks) и утечек горутин.