Содержание урока · 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: один флаг к одному ключу
GOvar 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, песочнице сайта недоступны ни аргументы командной строки, ни файлы.
ExecStart=/usr/local/bin/app --port 8080 в systemd-юните: Changed=true на каждом запуске, и значение флокировано выше файла. Через год в config.yaml ставят port: 9000 — а сервис по-прежнему слушает 8080. «Конфиг не работает», хотя конфиг исправен. Флаги в юнитах и скриптах деплоя оставляйте только для того, что обязано перекрывать всегда; всё остальное — файл и окружение.
Дефолт непереданного флага: самый слабый слой#
Интуиция подсказывает: раз флаг перекрывает всё, то и его дефолт перекрывает файл. В современном 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 выглядит правдоподобно, логи чистые, ошибки нет, а ключ из файла не читался никогда.
IsSet ищет ключ без «последнего шанса»: флаговый дефолт в её поиске не участвует. Отсюда расхождение: для ключа, живущего только в дефолте непереданного флага, viper.Get вернёт значение, а viper.IsSet — false. Валидация «нет ключа — падаем» уронит программу, хотя Get значение вернул. Лекарство простое: обязательные ключи дублируйте SetDefault — дефолты viper IsSet видит, а флаговый дефолт остаётся запасным выходом, а не единственным источником.
BindPFlags: набор целиком#
Привязывать флаги по одному необязательно: BindPFlags принимает целый FlagSet и связывает каждый флаг с ключом того же имени. --port ложится на ключ port, --max-backups — на max-backups, никаких преобразований, реплейсеры на флаги не действуют. Обычно привязывают персистентные флаги корня и локальные наборы команд, где реестр реально читается. Ограничение вытекает из механики: имя ключа равно имени флага, значит, плоские ключи покрываются полностью, а вложенный ключ server.port так не получить — флага с именем server.port нет. Раскладку по дереву YAML делает точечная BindPFlag из первого примера: флаг короткий, ключ составной. Комбинация рабочая: BindPFlags на все совпадающие имена плюс несколько точечных привязок там, где имена разошлись. Имена ключей в реестре приводятся к нижнему регистру, как и везде в viper, — но сами имена флагов регистрозависимы, Lookup ищет точное совпадение.
Каркас команды: OnInitialize и чтение через реестр
GOvar 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"))Соберите привязку флага
Допишите: набор флагов команды, привязку к ключу, поиск флага по имени и чтение из реестра.
Соберите связку по шагам
Расставьте строки: от объявления флага и дефолта до чтения значения из реестра.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: флаги в реестре
Флаг — последний внешний источник в реестре: окружение с префиксами и явными привязками разобрано в прошлом уроке, и теперь лестница собрана целиком — 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 с собранным бинарником.
Личные заметкиПусто
Вопросы и ответы по уроку
Флаг перекрывает файл всегда?
Нет, только переданный. Признак Changed viper проверяет при каждом Get: непереданный флаг не участвует в конкуренции, а его дефолт достаётся последним шансом — ниже файла, ниже KV-хранилища и ниже viper.SetDefault. Поэтому сам факт привязки флага конфиги не ломает — ломает постоянная передача флага скриптами.
BindPFlag или BindPFlags — что выбрать?
BindPFlags для массовой привязки: ключ равен имени флага, один вызов на FlagSet. BindPFlag — точечно: флаг называется port, а ключ конфига — server.port, имена раскладываются по дереву YAML. В свежем проекте планируйте имена так, чтобы хватило BindPFlags плюс пара точечных привязок на старые ключи.
Можно обойтись без viper и читать флаги напрямую?
Можно, и в крошечных утилитах это честный выбор. Но с появлением файла и окружения возникнет второй порядок приоритетов: код будет вручную решать, флаг важнее файла. Реестр viper держит порядок одним для всех ключей, а BindPFlag делает флаг обычным слоем этой лестницы.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Приоритеты источников: кто кого перекрывает
- Unmarshal: конфиг целиком в структуру
- Вложенные ключи: деревья и mapstructure
- WatchConfig и OnConfigChange: живой конфиг
- SetEnvPrefix и BindEnv: разруливаем коллизии
- Переменные окружения: AutomaticEnv
- SetDefault: значения по умолчанию
- Чтение значений: GetString, GetInt и семейство Get
- Все курсы по Go
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context