Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~18 мин +20 XP

BindPFlag: флаги командной строки в конфиге

Флаг командной строки должен перекрывать и файл, и окружение — viper BindPFlag поднимает его на второй слой реестра, проверяя flag.Changed при каждом чтении. Разберём связку с Cobra, BindPFlags для набора флагов, мост из stdlib flag и ловушку флагового дефолта, из-за которой config.yaml «не работает».

Содержание урока · 10 разделов

Пятничный прогон: нужно поднять сервис на другом порту, не трогая config.yaml в репозитории. Ответ напрашивается — флаг. Но флаг у нас объявлен в Cobra, а конфиг читает viper, и сейчас это два параллельных мира: RunE смотрит в cmd.Flags(), бизнес-код — в viper.GetInt. Порт, заданный флагом, не доходит до реестра, правки в конфиге неожиданно оказываются сильнее флага, а через полгода в коде заводится третий ручной приоритет — «флаг важнее файла, но не для логов». Мост между мирами — viper BindPFlag: привязанный флаг становится слоем реестра и встаёт на вторую ступень лестницы, сразу после Set, выше окружения и файла. Связка уже мелькала в курсе Cobra — там она работала в CLI-утилите; здесь разбираем её до винтиков: Changed, дефолты, IsSet и мост из stdlib flag.

Зачем флагам место в реестре#

Без привязки флаги и viper живут по разным правилам. Реестр держит лестницу приоритетов — Set, флаг, окружение, файл, key/value-хранилище, дефолты — один порядок на все ключи, разобранный ещё в первом уроке. Ручной перенос значений флагов в структуру конфига — паттерн, знакомый по уроку про флаги и конфигурацию, — этот порядок не отменяет, а дублирует: в одном месте код решает, что флаг важнее файла, в другом — что файл важнее, и каждый новый ключ требует нового решения. viper BindPFlag устраняет саму постановку вопроса: флаг перестаёт быть отдельным каналом и становится обычным слоем реестра. Читаете viper.GetInt("server.port") — реестр сам проверит флаг, потом окружение, потом файл, и решение не рассинхронизируется между частями программы. Бонусом сохраняется тип: Cobra объявляет флаги через pflag, и Int-флаг viper прочитает как число без строковых преобразований.

BindPFlag: один флаг к одному ключу

GO
var rootCmd = &cobra.Command{Use: "app"}

func init() {
        rootCmd.Flags().Int("port", 8080, "порт HTTP-сервера")

        viper.SetDefault("server.port", 7070)
        viper.SetConfigFile("config.yaml")
        if err := viper.ReadInConfig(); err != nil {
                log.Fatalf("конфиг: %v", err)
        }

        if err := viper.BindPFlag("server.port", rootCmd.Flags().Lookup("port")); err != nil {
                log.Fatal(err)
        }
}

// viper.GetInt("server.port"):
//   ./app              -> 9000 из файла: флаг не передан
//   ./app --port 7000  -> 7000 из флага

cmd.Flags().Lookup("port") возвращает *pflag.Flag — тот тип, которого ждёт BindPFlag. Привязка соединяет флаг с ключом, и имена могут различаться: короткий флаг port живёт в реестре под составным ключом server.port. Привязка живая — значение и признак Changed считываются в момент каждого Get, а не копируются при биндинге. Ошибку BindPFlag проверяем сразу: Lookup по опечатке вернёт nil, и без этой проверки привязка исчезнет молча.

flag.Changed: viper проверяет его сам#

Внутри каждого Get viper смотрит на поле Changed привязанного флага. Передан пользователем — значение флага возвращается сразу, на слое после Set: выше окружения, выше файла, выше всего. Не передан — viper проходит флаг мимо и идёт дальше по лестнице: окружение, файл, хранилище, дефолты. Проверять Changed вручную не нужно — в отличие от ручного переноса флагов в структуру, где каждая проверка пишется руками и забывается. Привязка не зависит и от порядка инициализации: BindPFlag можно вызвать хоть до ReadInConfig, хоть после — реестр читает флаг в момент Get, а не при привязке. Единственное условие: к моменту Get флаги должны быть разобраны; в Cobra разбор случается внутри Execute, до RunE, так что читать реестр в RunE — самое надёжное место. Как цепочка выглядит вживую — только на реальном бинарнике: соберите пример локальным Go и пощёлкайте флаг против config.yaml, песочнице сайта недоступны ни аргументы командной строки, ни файлы.

Дефолт непереданного флага: самый слабый слой#

Интуиция подсказывает: раз флаг перекрывает всё, то и его дефолт перекрывает файл. В современном viper это не так. Непереданный флаг из конкуренции исключается: пока Changed=false, его фактическое значение никого не интересует. Дефолт же достаётся последним шансом — только если ключ не нашёлся ни в Set, ни в окружении, ни в файле, ни в KV-хранилище, ни в SetDefault. То есть дефолт флага стоит ниже viper.SetDefault, в самом хвосте лестницы. Вопрос «почему BindPFlag перекрывает файл своим дефолтом» — один из самых частых по viper на StackOverflow; на современных версиях он не воспроизводится, но память форумов живёт долго, и «лечат» его обходными путями, которые ломают приоритеты ещё сильнее. Настоящие причины «config.yaml не работает» две. Первая — из врезки выше: скрипт деплоя всегда передаёт флаг. Вторая тише: опечатка в ключе файла. Опечатанный ключ не находится ни в одном слое — и последний шанс молча подставляет дефолт флага. Программа работает, значения правдоподобные, а конфиг по этому ключу мёртв.

Цепочка на четырёх сценариях

GO
// config.yaml: server: {port: 9000}
// флаг: --port, дефолт 8080, привязан к ключу server.port
viper.SetDefault("server.port", 7070)

viper.GetInt("server.port")

// флаг не передан, файл прочитан        -> 9000: файл выше дефолтов
// флаг не передан, файла нет            -> 7070: SetDefault выше дефолта флага
// флаг передан: ./app --port 7000       -> 7000: Changed=true, слой после Set
// в файле опечатка: server.prot: 9000   -> 8080: дефолт флага как последний шанс

Четыре сценария — весь стек в одном фрагменте. В первом файловые 9000 победили и SetDefault 7070, и флаговый дефолт 8080: файл стоит выше обоих. Последний сценарий самый коварный — 8080 выглядит правдоподобно, логи чистые, ошибки нет, а ключ из файла не читался никогда.

BindPFlags: набор целиком#

Привязывать флаги по одному необязательно: BindPFlags принимает целый FlagSet и связывает каждый флаг с ключом того же имени. --port ложится на ключ port, --max-backups — на max-backups, никаких преобразований, реплейсеры на флаги не действуют. Обычно привязывают персистентные флаги корня и локальные наборы команд, где реестр реально читается. Ограничение вытекает из механики: имя ключа равно имени флага, значит, плоские ключи покрываются полностью, а вложенный ключ server.port так не получить — флага с именем server.port нет. Раскладку по дереву YAML делает точечная BindPFlag из первого примера: флаг короткий, ключ составной. Комбинация рабочая: BindPFlags на все совпадающие имена плюс несколько точечных привязок там, где имена разошлись. Имена ключей в реестре приводятся к нижнему регистру, как и везде в viper, — но сами имена флагов регистрозависимы, Lookup ищет точное совпадение.

Каркас команды: OnInitialize и чтение через реестр

GO
var rootCmd = &cobra.Command{
        Use: "app",
        RunE: func(cmd *cobra.Command, args []string) error {
                fmt.Println("порт:", viper.GetInt("server.port"))
                return nil
        },
}

func init() {
        rootCmd.Flags().Int("port", 8080, "порт HTTP-сервера")
        viper.BindPFlags(rootCmd.Flags())
}

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

func initConfig() {
        viper.SetConfigFile("config.yaml")
        if err := viper.ReadInConfig(); err != nil {
                log.Fatal(err)
        }
}

BindPFlags связывает весь набор разом: ключ равен имени флага. OnInitialize запускает initConfig после разбора флагов, но привязка живая — реестр читает значения в момент Get, поэтому перестановка вызовов ничего не сломает. RunE работает только с viper: прямой cmd.Flags().GetInt рядом с реестром создал бы второй канал значений со своим порядком приоритетов, ради устранения которого всё и затевалось.

Правила связки без сюрпризов#

  • Привязывайте флаги до первого Get — не потому что иначе не сработает (привязка живая), а чтобы код читался без прыжков между этапами.
  • Проверяйте ошибку BindPFlag: Lookup по опечатке вернёт nil, привязка не случится, и флаг молча выпадет из реестра.
  • BindPFlags даёт ключ, равный имени флага: для плоских ключей хватит одного вызова, вложенные ключи раскладывает точечная BindPFlag.
  • Флаги — слой разовых перекрытий: всё, что меняется между окружениями, живёт в файле и окружении, а не в скриптах запуска.
  • Не читайте флаги напрямую в RunE мимо реестра: второй канал значений — второй порядок приоритетов, и рассинхрон только вопрос времени.

stdlib flag: мост через pflag#

Viper ждёт флаги pflag — тип, которым пользуется Cobra. Стандартный пакет flag напрямую не поддерживается, и README viper предлагает два пути. Первый — мост: pflag.CommandLine.AddGoFlagSet(flag.CommandLine) переливает stdlib-флаги в pflag, после pflag.Parse() привязывайте BindPFlags как обычно; признак Changed у перелитых флагов проставляется честно, при разборе. Код со старыми stdlib-флагами не переписывается. Второй путь — собственный адаптер: интерфейсы viper.FlagValue и viper.FlagValueSet — четыре метода (HasChanged, Name, ValueString, ValueType) и один (VisitAll), под stdlib-флаг они пишутся на полстраницы. Тонкость с Changed: у stdlib-флага такого поля нет, и в собственном адаптере признак изменения выводят косвенно — например, сравнением DefValue с текущим значением. Флаг, которому передали то же значение, что в дефолте, окажется «неизменённым», и файл конфигурации победит его законно. Для честного Changed берите pflag с самого начала — это и есть мост из первого пути.

Кто кого: флаг в лестнице приоритетов#

СлойЧто даётПример для server.port
1. Setпрограммная установка, сильнее всехviper.Set("server.port", 6000)
2. Переданный флагChanged=true: значение с командной строки./app --port 7000
3. Окружениепеременная из деплояAPP_SERVER_PORT=9000
4. Конфиг-файлбазовая настройка проектаport: 9000 в config.yaml
5. KV-хранилищеetcd и Consul через remote-провайдерключ в Consul
6. SetDefaultдефолты, объявленные кодомviper.SetDefault("server.port", 7070)
7. Дефолт флагапоследний шанс: Changed=false и ничего не нашлось8080 из объявления флага

Проверка: переданный флаг против файла

В config.yaml записан server.port: 9000. Флаг --port объявлен с дефолтом 8080 и привязан BindPFlag к ключу server.port. Пользователь запускает ./app --port 7000. Что вернёт viper.GetInt("server.port")?

Проверка: опечатка в Lookup

rootCmd.Flags().Lookup("prot") — опечатка в имени флага. Что произойдёт при вызове viper.BindPFlag("server.port", rootCmd.Flags().Lookup("prot"))?

Что выведет программа?

Прочитайте код и выберите, что он выведет:

// config.yaml не найден: ReadInConfig вернул ошибку
// флаг --port объявлен с дефолтом 8080 и привязан BindPFlag
// флаг не передан
viper.SetDefault("server.port", 7070)

fmt.Println(viper.GetInt("server.port"))

Соберите привязку флага

Допишите: набор флагов команды, привязку к ключу, поиск флага по имени и чтение из реестра.

rootCmd.().Int("port", 8080, "порт HTTP-сервера") err := viper.("server.port", rootCmd.Flags().("port")) if err != nil { log.Fatal(err) } port := viper.("server.port") fmt.Println(port)

Соберите связку по шагам

Расставьте строки: от объявления флага и дефолта до чтения значения из реестра.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: флаги в реестре

Термин

BindPFlag

нажмите, чтобы перевернуть
Определение

Точечная привязка флага к ключу: viper.BindPFlag("server.port", cmd.Flags().Lookup("port")). Имена флага и ключа могут различаться; при nil-флаге возвращает ошибку.

1 / 6

Флаг — последний внешний источник в реестре: окружение с префиксами и явными привязками разобрано в прошлом уроке, и теперь лестница собрана целиком — Set, флаг, env, файл, хранилище, дефолты, флаговый дефолт в хвосте. Дальше по курсу — сборка приоритетов в проверяемый тест, Unmarshal в структуру и горячая перезагрузка; полная карта тем — на странице курса viper. Прежде чем двигаться, соберите каркас из этого урока локально: cobra, аргументы командной строки и файловая система песочнице недоступны, а связка проверяется только реальным бинарником — ./app без флага, с флагом и с опечаткой в YAML. Три запуска дают больше уверенности, чем любой разбор: после них ловушка флагового дефолта перестаёт быть теорией.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • BindPFlag связывает флаг с ключом реестра: имена могут различаться, привязка живая — значение читается при каждом Get.
  • flag.Changed viper проверяет сам: переданный флаг стоит сразу после Set, выше окружения и файла.
  • Дефолт непереданного флага — самый слабый слой: ниже файла, KV-хранилища и viper.SetDefault.
  • Жалоба «флаговый дефолт перекрывает файл» на современном viper не воспроизводится; мёртвый конфиг — это всегда передаваемый флаг или опечатка в ключе.
  • IsSet ищет без последнего шанса: для ключа из дефолта флага Get вернёт значение, а IsSet — false.
  • BindPFlags привязывает набор целиком: ключ равен имени флага; вложенные ключи раскладывает точечная BindPFlag.
  • stdlib flag подключается мостом pflag.CommandLine.AddGoFlagSet или собственным адаптером FlagValue из четырёх методов.

Домашнее задание

Объявите флаг --port с дефолтом 8080, привяжите BindPFlag к ключу server.port, добавьте SetDefault 7070 и config.yaml с port: 9000. Прогоните три запуска: без флага, с --port 7000 и с опечатанным ключом в YAML — сверьте вывод с цепочкой из урока. Запуск только локальным Go с собранным бинарником.

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · BindPFlag: флаги командной строки в конфиге)

Вопросы и ответы по уроку

Флаг перекрывает файл всегда?

Нет, только переданный. Признак Changed viper проверяет при каждом Get: непереданный флаг не участвует в конкуренции, а его дефолт достаётся последним шансом — ниже файла, ниже KV-хранилища и ниже viper.SetDefault. Поэтому сам факт привязки флага конфиги не ломает — ломает постоянная передача флага скриптами.

BindPFlag или BindPFlags — что выбрать?

BindPFlags для массовой привязки: ключ равен имени флага, один вызов на FlagSet. BindPFlag — точечно: флаг называется port, а ключ конфига — server.port, имена раскладываются по дереву YAML. В свежем проекте планируйте имена так, чтобы хватило BindPFlags плюс пара точечных привязок на старые ключи.

Можно обойтись без viper и читать флаги напрямую?

Можно, и в крошечных утилитах это честный выбор. Но с появлением файла и окружения возникнет второй порядок приоритетов: код будет вручную решать, флаг важнее файла. Реестр viper держит порядок одним для всех ключей, а BindPFlag делает флаг обычным слоем этой лестницы.

Термины урока

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

Следующая ступень учебного пути

Chi25 уроков~9 чпрактика25 уроков уже доступны

Chi: роутер для Go HTTP-сервисов

Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.

  • Строить маршруты chi: методы, параметры пути, wildcards
  • Писать middleware и понимать порядок их выполнения
  • Группировать маршруты: Route, Group и подроутеры
  • Прокидывать данные в хендлеры через context.Context