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

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

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

Урок 9 из 25 ~16 мин +50 XP

Переменные окружения: AutomaticEnv

Viper AutomaticEnv включает переменные окружения как источник настроек одной строкой — это самый короткий путь к конфигурации в духе 12-factor. Разберём включение через AutomaticEnv, префикс SetEnvPrefix, замену символов SetEnvKeyReplacer, явную привязку BindEnv и то, как всё это живёт в docker-compose и CI.

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

Утилита работает на вашей машине и падает в контейнере. Локально порт брался из 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

GO
func 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 на время дебага

Отладка: переменная задана, а утилита её не видит#

Разбор такой жалобы всегда один и тот же, сверху вниз. Первый шаг: убедиться, что имя совпало байт в байт — 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.SetEnv("notes") // префикс утилиты viper.() // включаем проверку окружения replacer := strings.NewReplacer(".", "_") viper.SetEnvKey(replacer) // правило имени fmt.Println(viper.("server.port"))

Соберите включение окружения из шести строк

Расставьте: префикс, включатель, реплейсер, дефолт, чтение файла и печать значения.

Строки

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

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

Карточки: окружение в Viper

Термин

viper.AutomaticEnv

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

Включает проверку переменных окружения в каждом Get. Имя строится из префикса и ключа: server.port превращается в NOTES_SERVER_PORT. Ключи, о которых никто не спросил, в AllKeys не попадают.

1 / 5

Что дальше#

Окружение читается, но вопрос «кто кого перекрыл» остался открытым, потому что в этом уроке мы сознательно обходили его стороной: приоритеты конфигурации расставят флаг, 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: ключ без дефолта должен отсутствовать в списке, пока его не зарегистрируете.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Переменные окружения: AutomaticEnv)

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

Как заставить 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, файл, дефолт — разбирается в следующем уроке курса.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

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