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.
| Задача | stdlib | cobra + 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)
}
}
Подкоманда:
Код ITЗагрузка примера кода…
Разбор:
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.
Сериализация контактов — через encoding/json. Viper читает формат файла; доменные структуры по-прежнему unmarshaling в Go-типы.
embed и статика в бинарнике
//go:embed default.yaml
var defaultConfig []byte
func loadDefaults() {
viper.SetConfigType("yaml")
_ = viper.ReadConfig(bytes.NewReader(defaultConfig))
}
Разбор:
- Встроенный конфиг даёт работоспособный бинарник "из коробки"; пользовательский файл переопределяет значения.
- Подробнее — Модули, workspace, embed и slog.
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— см. Важные интерфейсы и типы Go.
Сигналы и 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-файлом — см. Тестирование в Go.
Ключевые тезисы
- cobra — дерево команд, валидация args, help и completion.
- viper — единая точка конфигурации (файл + env + флаги).
- stdout/stderr разделяют данные и диагностику.
signal.NotifyContextдаёт корректное завершение по Ctrl+C.
Мини-практикум
- Добавьте подкоманду
listс флагом--format=json|table. - Подключите viper: defaults в embed, override через
~/.phonebook/config.yaml. - Напишите тест
Execute()сSetArgsдля happy-path и неверного числа аргументов.
Типичные ошибки
| Ошибка | Что делать |
|---|---|
| Логи в stdout | Перенести в stderr / slog |
Один giant main() | Разнести по cmd/ и internal/ |
| Viper в каждой функции | Обёртка Config с явными полями |
Игнор RunE error | Всегда return err наверх |
Связанные материалы
- Модули, workspace, embed и slog
- Профилирование и fuzz — если CLI стал узким местом
- Веб на stdlib — эволюция phone book в HTTP-сервис
- gRPC в Go — внутренние RPC после REST
Базовый разбор HTTP — HTTP как основа веб-интеграций.