Содержание урока · 12 разделов
- AutomaticEnv: одна строка включает окружение
- SetEnvPrefix и конвенции имён переменных
- Правила именования переменных окружения
- BindEnv: явная привязка и нестандартные имена
- docker-compose и 12-factor: зачем всё это
- .env-файлы: промежуточный случай локальной разработки
- Куда что класть: файл против окружения
- Отладка: переменная задана, а утилита её не видит
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Утилита работает на вашей машине и падает в контейнере. Локально порт брался из notes.yaml, а в docker-compose рядом крутится другой сервис со своим портом, и файл туда положить негде: образ собирается один на все окружения, и перекомпилировать его ради номера порта никто не станет. Это классическая задача двенадцати факторов — конфигурация, которая меняется между деплоями, должна приходить из окружения, а не из файлов. Переменные окружения есть везде: в shell, в CI-пайплайнах, в docker-compose, в systemd-юнитах, в kubectl set env. Viper умеет их читать, но не делает этого по умолчанию — и это правильно: без настроенного префикса окружение резко расширяет поверхность конфликта имён. Включение занимает три вызова, и каждый из них нужен по своей причине.
AutomaticEnv: одна строка включает окружение#
viper.AutomaticEnv() — включатель. После него каждый Get проверяет окружение: viper.GetInt("server.port") сначала смотрит, задана ли переменная с подходящим именем, и только потом идёт дальше по слоям — к конфигу и дефолтам. Имя переменной собирается из ключа: точки и дефисы превращаются в подчёркивания, буквы поднимаются в верхний регистр. Ключ server.port соответствует переменной SERVER_PORT, ключ verbose — VERBOSE. Заметьте: сам ключ в реестре при этом не появляется — AutomaticEnv не регистрирует ничего, он только перехватывает запросы. Если утилита одна на машине и живёт в изоляции, без префикса можно жить; как только рядом появляется вторая утилита с тем же ключом, начинается конфликт имён — и его решает следующий вызов, SetEnvPrefix. Обратите внимание на слово «каждый Get»: окружение опрашивается в момент чтения, а не один раз при старте — это важно для понимания того, какие ключи вообще окажутся видимыми. Практический вывод из ленивости: читать значение до включения AutomaticEnv бессмысленно, а после включения любое обращение к известному ключу уже проверит переменную. Если переменная задана, она вернётся даже тогда, когда файл конфига существует и задаёт другое значение — окружение в цепочке стоит выше файла, и это поведение по умолчанию, а не ошибка.
Три вызова: AutomaticEnv, SetEnvPrefix, SetEnvKeyReplacer
GOfunc initConfig() {
viper.SetConfigName("notes")
viper.AddConfigPath(".")
// Окружение: префикс и правила построения имени
viper.SetEnvPrefix("notes")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Fprintln(os.Stderr, "конфиг не найден — беру дефолты и окружение")
} else {
fmt.Fprintln(os.Stderr, "ошибка конфига:", err)
os.Exit(1)
}
}
}SetEnvPrefix("notes") объявляет, что переменные утилиты начинаются с NOTES_: NOTES_SERVER_PORT, NOTES_VERBOSE. AutomaticEnv включает проверку окружения в каждом Get. SetEnvKeyReplacer ставит правило замены: точка в ключе становится подчёркиванием — именно она делает server.port читаемым как NOTES_SERVER_PORT. Без реплейсера Viper искала бы NOTES_SERVER.PORT — имя с точкой, которое shell не позволит задать в принципе. Порядок этих вызовов не важен, важно другое: всё должно выполниться до первого Get, поэтому код живёт в initConfig и выполняется перед Run любой команды.
SetEnvPrefix и конвенции имён переменных#
Префикс — не прихоть, а страховка от коллизий: PATH, HOME, PORT, USER уже заняты системой, и утилита, читающая переменную PORT без префикса, рано или поздно наткнётся на чужую. Посмотрите на большие CLI: docker читает DOCKER_HOST, kubectl — KUBECONFIG, gh — GH_TOKEN. Все они держат собственный префикс, и это часть публичного контракта утилиты, документированная наравне с флагами. Конвенция имени складывается из трёх частей: префикс утилиты, путь ключа, верхний регистр. NOTES_SERVER_PORT читается как «в NOTES переменная для server.port». Дефисы в ключах заменяются наравне с точками: ключ max-backups превращается в MAX_BACKUPS. Плоский ключ verbose с префиксом становится NOTES_VERBOSE, а не голым VERBOSE. Подводный камень — регистр: AutomaticEnv ищет переменную по строгому шаблону, и notes_server_port Viper не увидит, сколько ни экспортируйте; это самый частый вопрос в issues, и лечится он только правильным именем, не кодом.
Правила именования переменных окружения#
- Префикс утилиты обязателен: NOTES_SERVER_PORT вместо SERVER_PORT — нет коллизий с системными переменными
- Всё имя в верхнем регистре: notes_server_port Viper не найдёт, регистр строгий
- Точки и дефисы ключа заменяются подчёркиваниями: server.port превращается в NOTES_SERVER_PORT
- Одна переменная — один ключ: не собирайте пять настроек в одну строку через запятую
- Префикс — часть публичного контракта: DOCKER_HOST у docker, GH_TOKEN у gh, у вашей утилиты — NOTES_*
- Секреты живут только в окружении: токен в файле конфига попадает в бэкапы и git-историю
BindEnv: явная привязка и нестандартные имена#
AutomaticEnv строит имя по шаблону, а BindEnv связывает ключ с переменной напрямую — с любой, какую вы назовёте. viper.BindEnv("server.port", "NOTES_PORT") говорит: этот ключ читается из NOTES_PORT, шаблон не нужен. Вторая форма — viper.BindEnv("server.port") без второго аргумента: Viper сама построит имя из префикса и ключа, но, в отличие от ленивого AutomaticEnv, зарегистрирует ключ в реестре заранее, и он появится в AllKeys и AllSettings. Это различие решает известную проблему «невидимых» ключей: AutomaticEnv проверяет окружение только в момент, когда кто-то спрашивает конкретный ключ, а ключ, о котором никто не спросил, не попадает ни в списки настроек, ни в Unmarshal. BindEnv вызывается на каждый ключ отдельно, поэтому в больших конфигурациях им перечисляют только особые случаи, а остальное покрывают связкой AutomaticEnv плюс дефолты. Мой ориентир: всё, что задокументировано в README как переменная, должно быть либо дефолтом, либо BindEnv — документация обещает ключ, и реестр обязан его знать; иначе пользователь получит пустое значение ровно тогда, когда поверил документации.
BindEnv: три формы вызова
GO// Форма 1: имя строится из префикса и ключа
viper.BindEnv("server.port")
// Форма 2: нестандартное имя переменной
viper.BindEnv("server.port", "NOTES_PORT")
// Форма 3: список кандидатов — первая заданная побеждает
viper.BindEnv("server.port", "NOTES_PORT", "PORT")Первая форма равнозначна AutomaticEnv для одного ключа, плюс регистрирует его в AllKeys — этого уже достаточно, чтобы ключ увидел Unmarshal. Вторая форма нужна для унаследованных имён: CI давно раздаёт DATABASE_URL, и переписывать пайплайн ради переименования никто не будет. Третья — цепочка кандидатов: Viper берёт первую заданную переменную из списка, и короткий PORT может служить запасным вариантом. BindEnv работает и без AutomaticEnv — это независимый механизм, который можно включать точечно.
docker-compose и 12-factor: зачем всё это#
Честно про практику: главные потребители переменных окружения — не человек за ноутбуком, а CI и контейнеры. В docker-compose один образ конфигурируется окружением: тот же бинарник notes становится dev-версией с NOTES_SERVER_PORT=8080 и прод-версией с NOTES_SERVER_PORT=443, без пересборки. В GitHub Actions секреты приходят только окружением, и связка AutomaticEnv с префиксом ложится на это идеально. В systemd-юните ту же работу делает Environment=. Файл остаётся для того, что человек правит руками, — пути, таймауты, списки; окружение — для всего, что меняется между средами запуска. В этом и смысл двенадцатого фактора: конфигурация отделена от кода, потому что код один, а окружения разные. Правило раскладки простое: если значение меняется от машины к машине — окружение; если оно одинаково для всех запусков на этой машине — файл. Второй вывод следует из первого: чем меньше настроек меняется между средами, тем меньше переменных надо задавать в каждом деплое, поэтому дефолты в коде — не услужение, а экономия на каждом следующем окружении.
.env-файлы: промежуточный случай локальной разработки#
Локальная разработка живёт между двумя мирами: экспортировать десяток переменных в каждый новый терминал лень, а контейнер не нужен. Тут удобен .env-файл рядом с проектом: docker-compose умеет читать его через env_file, а чистый Go — через библиотеку godotenv, которая грузит файл в os.Environ до того, как Viper начнёт опрос. Важно понимать границы: .env-файл — это не механизм Viper, а костыль для удобства разработчика, и в продакшн его не берут. Файл с секретами не попадает в git — строка .env в .gitignore пишется раньше, чем сам файл, и проверяется это перед первым коммитом, а не после утечки. О тонкостях обращения с секретами и .env мы отдельно поговорим в уроке про watch и секреты; здесь достаточно правила: локально — .env, в CI и контейнерах — честные переменные окружения.
Куда что класть: файл против окружения#
| Настройка | Файл конфига | Переменная окружения |
|---|---|---|
| Порт, хост | дефолт для локальной разработки | основной способ в контейнере и CI |
| Пути к данным | да, человек правит руками | редко: путь фиксирован образом |
| Таймауты, лимиты | да | при точечной отладке на проде |
| Токены и пароли | никогда | только здесь |
| Уровень логов | как дефолт | часто: NOTES_LOG_LEVEL на время дебага |
AutomaticEnv ленив: окружение проверяется в момент Get конкретного ключа. Поэтому AllKeys не содержит ключей, существующих только в окружении, AllSettings их не вернёт, а viper.Unmarshal из десятого урока молча пропустит. Лекарство — SetDefault("server.port", 0) или BindEnv для каждого ключа: регистрация в реестре делает ключ видимым для списков. Заведите привычку объявлять дефолты для всех ключей — тогда окружение, файл и Unmarshal увидят одно и то же множество настроек. Один SetDefault на ключ стоит дешевле, чем час дебага пропавшей переменной.
Отладка: переменная задана, а утилита её не видит#
Разбор такой жалобы всегда один и тот же, сверху вниз. Первый шаг: убедиться, что имя совпало байт в байт — echo $NOTES_SERVER_PORT в том же shell, где запускается утилита; sudo, systemd и запускатели из IDE не наследуют ваш export, и это ловит половину случаев. Второй шаг: viper.AllKeys() — напечатайте список и найдите свой ключ; его там нет, значит ключ никто не зарегистрировал дефолтом или BindEnv, и ленивое окружение про него не знает. Третьего шага обычно не остаётся: если ключ зарегистрирован, переменная задана и имя совпало — Get вернёт значение. Полезная деталь для отладки: viper.IsSet("server.port") отвечает «задан ли ключ хоть в каком-то слое», не выдавая само значение. Ещё один источник сюрпризов — пустые значения: export NOTES_VERBOSE= оставляет переменную заданной, но пустой, и GetBool честно вернёт false, не отличив это от отсутствия. Вся цепочка проверяется только локально: соберите утилиту и пройдите её руками, браузерная песочница переменные не экспортирует.
Что вернёт GetInt при строковой переменной
Прочитайте код и выберите, что он выведет:
// Задано: export NOTES_SERVER_PORT=8080
viper.SetEnvPrefix("notes")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
fmt.Println(viper.GetInt("server.port"))
fmt.Println(viper.GetString("server.port"))Проверка: имя переменной из ключа
Настроены SetEnvPrefix("notes") и реплейсер точки на подчёркивание. Код вызывает viper.GetBool("server.timeouts.watch"). Какую переменную проверит Viper?
Проверка: окружение против файла
notes.yaml задаёт server.port = 8080, в терминале экспортирована NOTES_SERVER_PORT=9090, AutomaticEnv включён. Что вернёт viper.GetInt("server.port")?
Восстановите включение окружения
Впишите окончания методов и имя метода чтения.
Соберите включение окружения из шести строк
Расставьте: префикс, включатель, реплейсер, дефолт, чтение файла и печать значения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: окружение в Viper
Что дальше#
Окружение читается, но вопрос «кто кого перекрыл» остался открытым, потому что в этом уроке мы сознательно обходили его стороной: приоритеты конфигурации расставят флаг, env, файл и дефолты по местам, заодно привяжут флаги к реестру через BindPFlags и выгрузят всё в Config-структуру. Про тонкости секретов, .env-файлов и горячей перезагрузки — в уроке про watch и секреты. Обзор курса — на странице Cobra. И стандартная оговорка: окружение проверяется только локально — соберите утилиту, экспортируйте NOTES_SERVER_PORT и убедитесь, что она победила файл; песочница сайта переменные не экспортирует.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- AutomaticEnv включает окружение как источник настроек: каждый Get проверяет переменную с именем из префикса и ключа.
- SetEnvPrefix защищает от коллизий: NOTES_SERVER_PORT вместо SERVER_PORT, регистр строгий.
- SetEnvKeyReplacer превращает точки и дефисы ключа в подчёркивания — без него NOTES_SERVER.PORT не задать в shell.
- BindEnv даёт нестандартные имена, список кандидатов и регистрацию ключа в AllKeys.
- AutomaticEnv ленив: ключи, существующие только в окружении, не видны AllKeys и Unmarshal — лечится дефолтами или BindEnv.
- Docker-compose и CI — главные потребители окружения: один образ, разные переменные, без пересборки.
- Разбор «не видит переменную»: имя байт в байт, затем AllKeys показывает регистрацию ключа.
Домашнее задание
Добавьте в initConfig из восьмого урока SetEnvPrefix, AutomaticEnv и SetEnvKeyReplacer для точек и дефисов. Затем проведите три запуска: с файлом и переменной NOTES_SERVER_PORT, только с переменной и только с файлом — и запишите, какое значение победило в каждом случае. Проверьте AllKeys: ключ без дефолта должен отсутствовать в списке, пока его не зарегистрируете.
Личные заметкиПусто
Вопросы и ответы по уроку
Как заставить Viper читать переменные окружения?
viper.AutomaticEnv() включает проверку окружения в каждом Get. Имя переменной строится из SetEnvPrefix и ключа: с префиксом notes ключ server.port соответствует NOTES_SERVER_PORT. SetEnvKeyReplacer заменяет точки и дефисы на подчёркивания, чтобы имя было валидным для shell.
Почему viper.Unmarshal не видит переменные окружения?
Unmarshal идёт по AllKeys, а AutomaticEnv проверяет окружение лениво — только для явно запрошенных ключей. Ключ, существующий только в окружении, в AllKeys не попадает. Зарегистрируйте его через SetDefault или BindEnv — и Unmarshal увидит значение.
Что сильнее: переменная окружения или конфиг-файл?
Окружение. По умолчанию цепочка Viper ставит переменную выше файла: если NOTES_SERVER_PORT задана, её значение перекрывает порт из notes.yaml. Полная цепочка приоритетов — флаг, env, файл, дефолт — разбирается в следующем уроке курса.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
- Приоритеты конфигурации: флаг, env, файл
- Справка: кастомизация help и usage
- Хуки PreRun и PostRun: цепочки выполнения
- Ошибки и коды выхода: SilenceUsage
- Конфиг-файлы: YAML, TOML и JSON
- Знакомство с Viper: конфигурация без боли
- Флаги и структура конфига: паттерн binding
- Флаги: локальные и персистентные
- Все курсы по Go
Следующая ступень учебного пути
Bubble Tea: TUI-приложения в терминале
Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.
- Понимать Elm-архитектуру: Model, Update, View, Init
- Обрабатывать клавиатуру и мышь в терминале
- Писать асинхронные операции через tea.Cmd, Batch и Sequence
- Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner