Перейти к основному содержимому

CLI на cobra и viper

Разработчику Инженеру

Системное программирование в Go — отдельный пласт между «скриптом на Python» и «микросервисом». cobra строит дерево команд и флагов, viper объединяет конфиг из файлов и env. Статья дополняет модули и embed и обработку ошибок.

См. также: Первая программа · Простые приложения · Важные интерфейсы — io.Reader.


Зачем не только flag

Пакет flag из stdlib достаточен для одной команды. Для утилит с подкомандами (git commit, kubectl get), алиасами и автодокументацией берут spf13/cobra. Конфиг из YAML/JSON/env — spf13/viper. Так же устроены kubectl, hugo, docker compose.

Задачаstdlibcobra + viper
Один бинарник, 2 флагаflagИзбыточно
Подкоманды insert/list/deleteВручнуюcobra
Файл config + env overrideВручнуюviper
Shell completionНетcobra

Каркас cobra

var rootCmd = &cobra.Command{
Use: "phonebook",
Short: "Телефонная книга в CLI",
}

func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

Подкоманда:

var insertCmd = &cobra.Command{
Use: "insert [name] [phone]",
Short: "Добавить контакт",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
name, phone := args[0], args[1]
return store.Insert(name, phone)
},
}

func init() {
rootCmd.AddCommand(insertCmd)
insertCmd.Flags().StringVarP(&configPath, "config", "c", "phonebook.yaml", "путь к конфигу")
}

Разбор:

  • RunE возвращает error — cobra печатает её и выставляет exit code.
  • Args: cobra.ExactArgs(2) валидирует аргументы до вызова RunE.
  • Флаги регистрируют на конкретной команде; глобальные — на rootCmd.PersistentFlags().

viper — конфигурация

func initConfig() error {
viper.SetConfigName("phonebook")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.phonebook")
viper.AutomaticEnv()
viper.SetEnvPrefix("PHONEBOOK")
return viper.ReadInConfig()
}
# phonebook.yaml
data_file: ./contacts.json
log_level: info
path := viper.GetString("data_file")
level := viper.GetString("log_level")

Разбор:

  • Порядок приоритета (типичный): явные флаги > env > файл > defaults.
  • PHONEBOOK_DATA_FILE мапится на ключ data_file при SetEnvPrefix.
  • ReadInConfig может вернуть ошибку «файл не найден» — иногда это нормально, тогда задают defaults через viper.SetDefault.
JSON и YAML в stdlib

Сериализация контактов — через encoding/json. Viper читает формат файла; доменные структуры по-прежнему unmarshaling в Go-типы.


embed и статика в бинарнике

//go:embed default.yaml
var defaultConfig []byte

func loadDefaults() {
viper.SetConfigType("yaml")
_ = viper.ReadConfig(bytes.NewReader(defaultConfig))
}

Разбор:

  • Встроенный конфиг даёт работоспособный бинарник «из коробки»; пользовательский файл переопределяет значения.
  • Подробнее — 29.md.

stdin, stdout, stderr

CLI-утилиты следуют UNIX-конвенции:

ПотокНазначение
stdoutРезультат для pipe (phonebook list | jq)
stderrЛоги и ошибки
stdinДанные pipe (cat names | phonebook import)
data, err := io.ReadAll(os.Stdin)

Разбор:

  • Логи через slog пишут в stderr, чтобы не ломать JSON на stdout.
  • io.Reader / io.Writer — см. 23.md.

Сигналы и graceful exit

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if err := rootCmd.ExecuteContext(ctx); err != nil {
os.Exit(1)
}

Разбор:

  • ExecuteContext передаёт отмену в RunE — долгие операции проверяют ctx.Done().
  • На Windows syscall.SIGTERM может отсутствовать; для кроссплатформенных утилит достаточно os.Interrupt.

Пример дерева команд (phone book)

КомандаДействие
phonebook insert NAME PHONEдобавить
phonebook search NAMEнайти
phonebook listвывести все
phonebook delete NAMEудалить

Персистентность — JSON-файл; позже тот же домен уходит в веб-сервис и REST.


Тестирование CLI

func TestInsertCmd(t *testing.T) {
cmd := insertCmd
cmd.SetArgs([]string{"Ann", "+1-555-0100"})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
err := cmd.Execute()
if err != nil {
t.Fatal(err)
}
}

Разбор:

  • SetArgs имитирует argv без subprocess.
  • Store подменяют интерфейсом или temp-файлом — см. 192.md.

Ключевые тезисы

  • cobra — дерево команд, валидация args, help и completion.
  • viper — единая точка конфигурации (файл + env + флаги).
  • stdout/stderr разделяют данные и диагностику.
  • signal.NotifyContext даёт корректное завершение по Ctrl+C.

Мини-практикум

  1. Добавьте подкоманду list с флагом --format=json|table.
  2. Подключите viper: defaults в embed, override через ~/.phonebook/config.yaml.
  3. Напишите тест Execute() с SetArgs для happy-path и неверного числа аргументов.

Типичные ошибки

ОшибкаЧто делать
Логи в stdoutПеренести в stderr / slog
Один giant main()Разнести по cmd/ и internal/
Viper в каждой функцииОбёртка Config с явными полями
Игнор RunE errorВсегда return err наверх

Связанные материалы


Основа по протоколу

Базовый разбор HTTP — HTTP как основа веб-интеграций.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").