Создание CLI-приложений на Go с использованием Cobra и Viper

Разработка CLI на Go с фреймворками Cobra и Viper

Содержимое страницы

Интерфейсы командной строки (CLI) являются важными инструментами для разработчиков, системных администраторов и специалистов DevOps. Два библиотеки Go стали де-факто стандартом для разработки CLI на Go: Cobra для структуры команд и Viper для управления конфигурацией.

Go стал отличным языком для создания инструментов CLI благодаря своей производительности, простому развертыванию и кроссплатформенной поддержке.

Tetris

Почему стоит выбирать Go для приложений CLI

Go предлагает убедительные преимущества для разработки CLI:

  • Единый бинарный файл: Нет необходимости в среде выполнения или менеджерах пакетов
  • Быстрое выполнение: Нативная компиляция обеспечивает отличную производительность
  • Кроссплатформенная поддержка: Легкая компиляция для Linux, macOS, Windows и других платформ
  • Богатая стандартная библиотека: Инструменты для работы с файлами, сетью и текстовыми данными
  • Конкуренция: Встроенные горутины для параллельных операций
  • Статическая типизация: Обнаружение ошибок на этапе компиляции

Популярные инструменты CLI, созданные на Go, включают Docker, Kubernetes (kubectl), Hugo, Terraform и GitHub CLI. Если вы новичок в Go или вам нужна быстрая справка, ознакомьтесь с нашим Go Cheat Sheet для основного синтаксиса и шаблонов.

Введение в Cobra

Cobra - это библиотека, предоставляющая простой интерфейс для создания мощных современных приложений CLI. Созданная Стивом Францией (spf13), тем же автором, что и Hugo и Viper, Cobra используется во многих популярных проектах на Go.

Основные возможности Cobra

Структура команд: Cobra реализует шаблон команд, позволяя создавать приложения с командами и подкомандами (как git commit или docker run).

Обработка флагов: Локальные и постоянные флаги с автоматическим разбором и преобразованием типов.

Автоматическая справка: Генерирует текст справки и информацию об использовании автоматически.

Интеллектуальные подсказки: Предоставляет подсказки при опечатках (“Вы имели в виду ‘status’?”).

Автодополнение оболочки: Генерация скриптов автодополнения для bash, zsh, fish и PowerShell.

Гибкий вывод: Работает плавно с пользовательскими форматировщиками и стилями вывода.

Введение в Viper

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

Основные возможности Viper

Множественные источники конфигурации:

  • Конфигурационные файлы (JSON, YAML, TOML, HCL, INI, envfile, Java properties)
  • Переменные окружения
  • Флаги командной строки
  • Удаленные системы конфигурации (etcd, Consul)
  • Значения по умолчанию

Порядок приоритета конфигурации:

  1. Явные вызовы Set
  2. Флаги командной строки
  3. Переменные окружения
  4. Конфигурационный файл
  5. Хранилище ключ-значение
  6. Значения по умолчанию

Мониторинг изменений: Отслеживание конфигурационных файлов и автоматическая перезагрузка при изменениях.

Преобразование типов: Автоматическое преобразование в различные типы Go (строка, int, bool, duration и т.д.).

Начало работы: Установка

Сначала инициализируйте новый модуль Go и установите обе библиотеки:

go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest

По желанию установите генератор CLI Cobra для создания шаблонов:

go install github.com/spf13/cobra-cli@latest

Создание первого приложения CLI

Давайте создадим практический пример: инструмент управления задачами CLI с поддержкой конфигурации.

Структура проекта

mytasks/
├── cmd/
│   ├── root.go
│   ├── add.go
│   ├── list.go
│   └── complete.go
├── config/
│   └── config.go
├── main.go
└── config.yaml

Основная точка входа

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Корневая команда с интеграцией Viper

// cmd/root.go
package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "mytasks",
    Short: "Простое CLI для управления задачами",
    Long: `MyTasks - это CLI менеджер задач, который помогает вам организовывать
ежедневные задачи с легкостью. Создано с использованием Cobra и Viper.`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    cobra.OnInitialize(initConfig)

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
        "конфигурационный файл (по умолчанию $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db",
        "расположение файла базы данных")

    viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }

        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".mytasks")
    }

    viper.SetEnvPrefix("MYTASKS")
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Используется конфигурационный файл:", viper.ConfigFileUsed())
    }
}

Добавление подкоманд

// cmd/add.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var priority string

var addCmd = &cobra.Command{
    Use:   "add [описание задачи]",
    Short: "Добавить новую задачу",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        db := viper.GetString("database")

        fmt.Printf("Добавление задачи: %s\n", task)
        fmt.Printf("Приоритет: %s\n", priority)
        fmt.Printf("База данных: %s\n", db)

        // Здесь вы бы реализовали фактическое сохранение задачи
    },
}

func init() {
    rootCmd.AddCommand(addCmd)

    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium",
        "Приоритет задачи (низкий, средний, высокий)")
}

Команда списка

// cmd/list.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var showCompleted bool

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "Показать все задачи",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")

        fmt.Printf("Показ задач из: %s\n", db)
        fmt.Printf("Показывать выполненные: %v\n", showCompleted)

        // Здесь вы бы реализовали фактический вывод списка задач
    },
}

func init() {
    rootCmd.AddCommand(listCmd)

    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false,
        "Показывать выполненные задачи")
}

Реализация сохранения данных

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

  • SQLite: Легковесная, серверная база данных, идеальная для инструментов CLI
  • PostgreSQL/MySQL: Полнофункциональные базы данных для более сложных приложений
  • Файлы JSON/YAML: Простое файловое хранилище для легковесных нужд
  • Встроенные базы данных: BoltDB, BadgerDB для хранения ключ-значение

Если вы работаете с реляционными базами данных, такими как PostgreSQL, вам захочется использовать ORM или конструктор запросов. Для всестороннего сравнения библиотек баз данных Go, ознакомьтесь с нашим руководством Сравнение Go ORMs для PostgreSQL: GORM vs Ent vs Bun vs sqlc.

Расширенная конфигурация с Viper

Пример конфигурационного файла

# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

Чтение вложенной конфигурации

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")

    fmt.Printf("Уведомления включены: %v\n", enabled)
    fmt.Printf("Звук включен: %v\n", sound)
}

func getPriorityColors() map[string]string {
    return viper.GetStringMapString("priorities")
}

Переменные окружения

Viper автоматически считывает переменные окружения с заданным префиксом:

export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list

Живая перезагрузка конфигурации

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Конфигурационный файл изменен:", e.Name)
        // Перезагрузите компоненты, зависящие от конфигурации
    })
}

Лучшие практики

1. Используйте генератор Cobra для согласованности

cobra-cli init
cobra-cli add serve
cobra-cli add create

2. Организуйте команды в отдельных файлах

Храните каждую команду в своем файле в директории cmd/ для удобства поддержки.

3. Используйте постоянные флаги

Используйте постоянные флаги для опций, которые применяются ко всем подкомандам:

rootCmd.PersistentFlags().StringP("output", "o", "text",
    "Формат вывода (текст, json, yaml)")

4. Реализуйте правильную обработку ошибок

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "Мое приложение",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("не удалось выполнить что-то: %w", err)
        }
        return nil
    },
}

5. Предоставляйте осмысленную справку

var cmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Развертывание приложения в указанной среде",
    Long: `Развертывание собирает и развертывает ваше приложение в
указанной среде. Поддерживаемые среды:
  - development (dev)
  - staging
  - production (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. Устанавливайте разумные значения по умолчанию

func init() {
    viper.SetDefault("port", 8080)
    viper.SetDefault("timeout", "30s")
    viper.SetDefault("retries", 3)
}

7. Проверяйте конфигурацию

func validateConfig() error {
    port := viper.GetInt("port")
    if port < 1024 || port > 65535 {
        return fmt.Errorf("недопустимый порт: %d", port)
    }
    return nil
}

Тестирование CLI Приложений

Тестирование Команд

// cmd/root_test.go
package cmd

import (
    "bytes"
    "testing"
)

func TestRootCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetArgs([]string{"--help"})

    if err := cmd.Execute(); err != nil {
        t.Fatalf("command execution failed: %v", err)
    }
}

Тестирование с Разными Конфигурациями

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)

    // Запустите свои тесты

    viper.Reset() // Очистка
}

Генерация Автодополнений для Shell

Cobra может генерировать скрипты автодополнения для различных оболочек:

// cmd/completion.go
package cmd

import (
    "os"
    "github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Генерация скрипта автодополнения",
    Long: `Для загрузки автодополнений:

Bash:
  $ source <(mytasks completion bash)

Zsh:
  $ source <(mytasks completion zsh)

Fish:
  $ mytasks completion fish | source

PowerShell:
  PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
    DisableFlagsInUseLine: true,
    ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
    Args: cobra.ExactValidArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        switch args[0] {
        case "bash":
            cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}

Сборка и Распространение

Сборка для Разных Платформ

# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64

# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64

# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe

Уменьшение Размера Бинарного Файла

go build -ldflags="-s -w" -o mytasks

Добавление Информации о Версии

// cmd/version.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var (
    Version   = "dev"
    Commit    = "none"
    BuildTime = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Вывод информации о версии",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Версия: %s\n", Version)
        fmt.Printf("Коммит: %s\n", Commit)
        fmt.Printf("Собран: %s\n", BuildTime)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

Сборка с информацией о версии:

go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
    -X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
    -X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
    -o mytasks

Реальные Примеры

Популярные открытые инструменты, созданные с использованием Cobra и Viper:

  • kubectl: Инструмент командной строки Kubernetes
  • Hugo: Генератор статических сайтов
  • GitHub CLI (gh): Официальный CLI GitHub
  • Docker CLI: Управление контейнерами
  • Helm: Менеджер пакетов Kubernetes
  • Skaffold: Инструмент рабочего процесса разработки Kubernetes
  • Cobra CLI: Самохозяин - Cobra использует себя!

Помимо традиционных инструментов DevOps, возможности CLI на Go распространяются на приложения искусственного интеллекта и машинного обучения. Если вас интересует создание CLI-инструментов, взаимодействующих с большими языковыми моделями, ознакомьтесь с нашим руководством Ограничение LLMs с помощью структурированного вывода с использованием Ollama и Go, которое показывает, как создавать приложения на Go, работающие с моделями ИИ.

Полезные Ресурсы

Заключение

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

Эта комбинация позволяет создавать CLI-инструменты, которые:

  • Легки в использовании с интуитивными командами и автоматической справкой
  • Гибкие с несколькими источниками конфигурации
  • Профессиональные с автодополнением оболочки и правильной обработкой ошибок
  • Легко поддерживаемые с чистой организацией кода
  • Переносимые на разные платформы

Будь то создание инструментов для разработчиков, системных утилит или автоматизации DevOps, Cobra и Viper предоставляют прочную основу, необходимую для создания CLI-приложений, которые пользователи будут любить.