Приходилось ли вам писать обертки (wrappers) над CLI-утилитами или внешними API? Расскажите, зачем это нужно и приведите пример.

Ответ

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

Основные причины для создания оберток:

  1. Инкапсуляция и упрощение: Скрытие сложных вызовов, флагов и форматов вывода за простым и понятным методом. Это реализация паттерна "Фасад".
  2. Типизация: Преобразование строковых аргументов и выводов в строго типизированные структуры Go, что уменьшает количество ошибок.
  3. Управление зависимостями: Зависимость от внешнего инструмента концентрируется в одном месте (в обертке), а не размазывается по всей кодовой базе.
  4. Тестируемость: Обертку легко подменить моком (mock) в unit-тестах. Для этого создается интерфейс, который реализует как реальная обертка, так и тестовая заглушка. Напрямую тестировать код, вызывающий exec.Command, гораздо сложнее.
  5. Обработка ошибок: Централизованная и более осмысленная обработка ошибок, возвращаемых утилитой или API.

Пример: обертка над CLI-утилитой ffmpeg

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

import (
    "context"
    "fmt"
    "os/exec"
)

// Converter определяет интерфейс для тестирования и мокирования.
type Converter interface {
    Convert(ctx context.Context, inputPath, outputPath string) error
}

// FFmpegWrapper реализует интерфейс Converter.
type FFmpegWrapper struct {
    binPath string // Путь к исполняемому файлу ffmpeg
}

// NewFFmpegWrapper создает новый экземпляр обертки.
func NewFFmpegWrapper(binPath string) *FFmpegWrapper {
    return &FFmpegWrapper{binPath: binPath}
}

// Convert выполняет конвертацию видео.
func (f *FFmpegWrapper) Convert(ctx context.Context, inputPath, outputPath string) error {
    // Пример команды: ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4
    cmd := exec.CommandContext(ctx, f.binPath,
        "-i", inputPath,
        "-c:v", "libx264",
        "-crf", "23",
        outputPath,
    )

    output, err := cmd.CombinedOutput() // Получаем stdout и stderr
    if err != nil {
        return fmt.Errorf("ошибка выполнения ffmpeg: %w, вывод: %s", err, string(output))
    }

    return nil
}

Такой подход позволяет легко использовать конвертер в коде и так же легко заменить его на мок в тестах, не вызывая реальный ffmpeg.