---
title: "Переменные окружения: AutomaticEnv"
description: "viper AutomaticEnv: переменные окружения Go перекрывают файл конфига. EnvKeyReplacer и SERVER_PORT, строки вместо чисел, ловушка AllKeys. Примеры и практика."
course: "Viper: конфигурация приложений на Go"
url: "https://golangmaster.ru/courses/viper/lessons/06-okruzhenie/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 18
---

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

*Курс «Viper: конфигурация приложений на Go», урок 6 из 25. ~18 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/viper/lessons/06-okruzhenie/*

Viper AutomaticEnv включает проверку переменных окружения при каждом чтении ключа: деплой передаёт настройки через env, и файл конфига остаётся лишь базой. Разберём EnvKeyReplacer и превращение server.port в SERVER_PORT, строки вместо чисел, приоритет окружения над файлом и слепую зону AllKeys, из-за которой дамп показывает пустой конфиг при живом сервисе.

Пятница, выкатка. DevOps говорит: «в нашем Kubernetes конфиг-файлы не монтируем — настройки передаём переменными окружения». А ваш сервис читает config.yaml с портом стенда, и пересобирать бинарник под каждый контур — тот путь, от которого ушли ещё в первом уроке. [Дефолты прошлого урока](/courses/viper/lessons/05-setdefault/) решают только половину проблемы: программа запускается, но настройки деплоя до неё всё ещё не доходят. Вторую половину закрывает viper AutomaticEnv: одна строка кода учит реестр заглядывать в переменные процесса при каждом чтении. Деплой задаёт порт, хост базы и уровень логов, не трогая ни код, ни файлы, — а viper сам решает, что перекрылось. Разберём механику, преобразование имён, приоритет env над файлом и одну слепую зону, которая регулярно заводит инженеров в тупик.

### viper AutomaticEnv: как это работает

AutomaticEnv не копирует переменные в реестр заранее — она меняет поведение каждого чтения. На Get, GetString, GetInt и IsSet viper спускается по лестнице приоритетов и на уровне окружения делает следующее: берёт ваш ключ, приводит его к верхнему регистру и ищет переменную с таким именем через os.LookupEnv. Запрос server.port превращается в поиск SERVER.PORT — и вот первая засада: переменную с точкой в имени shell не выставит, export SERVER.PORT=9090 bash отклонит как невалидный идентификатор. Поэтому без настройки имён AutomaticEnv в одиночку находит только ключи без точек: retries, debug, loglevel. Регистр работает в обе стороны: ключ в коде пишется как угодно — Server.Port, SERVER.PORT — реестр приведёт его к нижнему регистру, а поиск в окружении всегда идёт в верхнем. Запомните порядок: верхний регистр, потом замена разделителей, потом поиск. Проверку окружения можно и точечно: для ручного управления существует BindEnv, но в большинстве сервисов одной AutomaticEnv хватает, а тонкости явной привязки — тема ближайшего урока.

### AutomaticEnv без настройки имён

```go
viper.AutomaticEnv()
viper.SetDefault("retries", 3)
viper.SetDefault("server.port", 8080)

// запуск: RETRIES=5 ./app
fmt.Println(viper.GetInt("retries"))     // 5 — env найден
fmt.Println(viper.GetInt("server.port")) // 8080 — SERVER.PORT не выставить
```

retries прочитался из окружения: ключ без точек после приведения к регистру становится именем переменной RETRIES. Вложенный server.port ищется как SERVER.PORT — а такое имя shell не даёт создать, и viper тихо спускается к дефолту. Чинится заменой разделителей — следующий блок.

### SetEnvKeyReplacer: точки становятся подчёркиваниями

Связка SetEnvKeyReplacer(strings.NewReplacer(".", "_")) описывает, как превращать ключ в имя переменной: viper берёт ключ в верхнем регистре и прогоняет через замену — server.port становится SERVER_PORT, database.host — DATABASE_HOST. Replacer — обычный strings.NewReplacer, тот же тип, что и для массовых замен строк: можно заменить не только точки, но и дефисы. Правило одно: SetEnvKeyReplacer должен быть вызван до первых чтений, и обычно его ставят рядом с AutomaticEnv в начале конфигурационной секции. После этого связка полностью предсказуема: ключ → верхний регистр → замена разделителей → поиск в os.LookupEnv. Нашли — значение вернётся с этого слоя; не нашли — viper пойдёт дальше по файлу и дефолтам, как будто окружения вообще нет.

### Рабочая связка: replacer, AutomaticEnv, дефолты

```go
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("server.port", 8080)

// запуск: SERVER_PORT=9090 ./app
port := viper.GetInt("server.port")
fmt.Println(port) // 9090
```

Три строки подготовки и одно чтение. Ключ server.port приводится к SERVER.PORT, replacer заменяет точку — viper проверяет SERVER_PORT через os.LookupEnv, находит строку "9090" и cast приводит её к int. Уберите переменную — вернётся дефолт 8080: окружение перекрывает, но не ломает. Проверяется локальным Go: go build, затем запуск с переменной перед бинарником.

В окружении процесса не существует чисел и булевых значений — всё строки. SERVER_PORT=9090 в памяти процесса лежит как "9090", а не как int. Для viper это не проблема: cast приводит строку к нужному типу на чтении, и правила те же, что для файла — GetInt справится с "9090", GetBool поймёт "true", "1" и "t", GetDuration распарсит "30s". Из строковости следуют две практики. Первая: не кладите в env сырые структуры — список придётся передавать строкой с разделителями, и GetStringSlice разобьёт её по пробелам. Вторая: валидация не отменяется — GetInt("8080порт") вернёт ноль так же молча, как и для конфиг-файла. Отдельный нюанс: пустая переменная SERVER_PORT="" по умолчанию считается несуществующей — viper пропустит её и возьмёт значение ниже по лестнице. Поведение переключается AllowEmptyEnv, но включать пустоту как легальное значение стоит осознанно. Кстати, у файла поведение другое: yaml со значением port: "" вернёт пустую строку как полноценное значение, а не провал к дефолту — окружение и файл согласованы не во всём.

### Слепая зона: AllKeys и AllSettings не видят окружение

Самая коварная особенность AutomaticEnv: значения читаются, а ключей в реестре нет. AllKeys собирает ключи из файлов, дефолтов, Set и явно привязанных env — но перечислить все переменные процесса он не может и не пытается: viper не знает, какие из тысяч системных переменных относятся к вашей конфигурации. Поэтому AllSettings покажет только файл и дефолты, и дамп на старте напечатает «пустой конфиг» для сервиса, который прекрасно работает на env-настройках. Отсюда правило: не используйте AllSettings для логирования конфигурации целиком — печатайте конкретные ключи через Get, они честно сходят в окружение. Явная привязка [BindEnv](/courses/viper/lessons/07-env-prefix/) регистрирует ключ в реестре — и тогда он появляется в AllKeys вместе со значением; вместе с префиксами это тема следующего урока.

### Дамп, который вводит в заблуждение

```go
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")

// запуск: SERVER_PORT=9090 ./app
fmt.Println(viper.AllSettings())      // map[log.level:info server.port:8080]
fmt.Println(viper.Get("server.port")) // 9090 — из окружения
```

AllSettings отчитался дефолтами, а Get в соседней строке вернул 9090 из переменной — противоречие налицо, и это не баг, а следствие дизайна. Для дампа «как поедет сервис» печатайте конкретные ключи через Get, а не AllSettings целиком. Воспроизведите у себя локально — две минуты опыта против вечера отладки в проде.

> **«Пустой конфиг» при работающем сервисе.** Классический инцидент: инженер на дежурстве печатает на старте AllSettings, видит дефолтный порт 8080 и делает вывод, что переменные окружения не применились. Сервис при этом слушает 9090 из SERVER_PORT — просто дамп слеп к окружению. Обратная сторона той же медали: цикл валидации по AllKeys пропустит env-настроенные ключи, и обязательная настройка окажется непроверенной. Лечится BindEnv — регистрацией ключей.

### Что видит API viper по каждому источнику

| Источник | Get и IsSet | AllKeys | AllSettings |
| --- | --- | --- | --- |
| Конфиг-файл | да | да | да |
| SetDefault | да | да | да |
| viper.Set | да | да | да |
| env: AutomaticEnv | да | нет | нет |
| env: BindEnv | да | да | да |

На лестнице приоритетов окружение стоит между флагом и конфиг-файлом: переменная перекрывает и файл, и дефолт, но уступает флагу и явному Set. Практический смысл ровно такой, каким его задумали в двенадцати факторах: файл в репозитории описывает разумную базу, деплой уточняет её переменными, и один бинарник едет во все контуры без пересборки. Дефолты из прошлого урока при этом не теряют ценности: они гарантируют запуск без окружения — локально, в тестах, в одноразовом контейнере. Одного нюанса хватает на целый следующий урок: два сервиса на одной машине хотят переменную PORT каждый по-своему, и решается это префиксом SetEnvPrefix — PORT превращается в APP_PORT. Здесь достаточно знать, что механизм существует и что до него у окружения есть слепая зона из предыдущего раздела.

> **IsSet тоже проверяет окружение.** AutomaticEnv включена — IsSet отвечает true и для значений из переменных, поэтому обязательные настройки проверяются на старте честно: if !viper.IsSet("api.key") { log.Fatal("задайте API_KEY") }. Сообщение с именем переменной окружения вместо имени ключа экономит дежурному десять минут: он сразу видит, что экспортировать.

### Гигиена env-конфигурации

- SetEnvKeyReplacer(strings.NewReplacer(".", "_")) — в начале конфигурационной секции: без него вложенные ключи не найдут переменные.
- Дефолты SetDefault остаются обязательными: без окружения и файла запуск должен давать разумные значения, а не нули.
- Окружение — всегда строки: GetInt и GetBool приведут сами, но границы значений проверяет ваша валидация.
- Не логируйте конфиг через AllSettings: env-ключи в дамп не попадут — печатайте критичные ключи через Get.
- Пустая переменная по умолчанию считается несуществующей: SERVER_PORT="" уступит дефолту, пока не включён AllowEmptyEnv.
- Секреты через env передавать можно, печатать их в лог на старте — нельзя: маскируйте значения в дампах.

### Проверка: имя переменной

**Вопрос:** AutomaticEnv включена, задан SetEnvKeyReplacer(strings.NewReplacer(".", "_")). Какую переменную окружения проверит viper.Get("server.port")?

- SERVER.PORT: viper приводит ключ к верхнему регистру и ищет как есть
- ✅ SERVER_PORT: сначала верхний регистр, затем замена точки на подчёркивание
- server_port: viper сохраняет нижний регистр ключа
- PORT: секция server отбрасывается как префикс

Порядок фиксирован: ключ приводится к верхнему регистру, потом replacer заменяет разделители, потом идёт поиск в os.LookupEnv. Без replacer искалось бы SERVER.PORT — имя, которое shell не выставит, и viper молча ушёл бы к файлу и дефолтам.

### Проверка: дамп на старте

**Вопрос:** Сервис с AutomaticEnv работает и берёт значения из переменных окружения. Что покажет viper.AllSettings()?

- Полную конфигурацию, включая значения из окружения
- ✅ Только файл и дефолты: env-значения в AllSettings не попадают
- Только значения из окружения
- Ошибку: AllSettings несовместим с AutomaticEnv

AllSettings строится на AllKeys, а тот собирает лишь зарегистрированные ключи: файлы, дефолты, Set, BindEnv. Перечислять переменные процесса viper не умеет. Для полной картины печатайте конкретные ключи через Get или регистрируйте их BindEnv.

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

```go
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetDefault("retries", 3)

// запуск: RETRIES=5 ./app
fmt.Println(viper.GetInt("retries"))
```

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

- ✅ 5: окружение перекрывает дефолт, строка «5» приведена к int
- 3: дефолт задан в коде и сильнее окружения
- 0: строку «5» нельзя привести к числу
- Ошибку: тип переменной не совпадает с типом дефолта

На лестнице окружение стоит выше дефолта, поэтому RETRIES=5 победил. Строковость env не помешала: cast вызвал strconv.Atoi. Заметьте, что без replacer этот пример тоже работал бы — ключ retries без точек и так превращается в RETRIES.

### Соберите чтение из окружения

Допишите включение AutomaticEnv, создание replacer и чтение числа.

```go
viper.___()
viper.SetEnvKeyReplacer(strings.___(".", "_"))

// запуск: SERVER_PORT=9090 ./app
port := viper.___("server.port")
fmt.Println(port) // 9090
```

Подсказка: Проверку окружения включает AutomaticEnv, replacer создаёт strings.NewReplacer, число читает GetInt.

Правильные ответы в пропусках: `AutomaticEnv`, `NewReplacer`, `GetInt`

### Соберите env-секцию конфигурации

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

Правильный порядок строк:

1. `replacer := strings.NewReplacer(".", "_")`
2. `viper.SetEnvKeyReplacer(replacer)`
3. `viper.AutomaticEnv()`
4. `viper.SetDefault("server.port", 8080)`
5. `port := viper.GetInt("server.port")`
6. `fmt.Println("порт:", port)`

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

- **AutomaticEnv** — viper.AutomaticEnv() — на каждом чтении ключ проверяется в переменных окружения. Ничего не копирует заранее: проверка происходит в момент Get и IsSet.
- **EnvKeyReplacer** — SetEnvKeyReplacer(strings.NewReplacer(".", "_")) — превращает server.port в SERVER_PORT. Без него вложенные ключи ищутся как SERVER.PORT, а такое имя shell не выставит.
- **Имя переменной** — Порядок: ключ в верхнем регистре, замена разделителей replacer-ом, при наличии префикса он добавляется. Итог: SERVER_PORT, а с префиксом APP_SERVER_PORT.
- **Строковость env** — Переменные окружения всегда строки: GetInt вызовет strconv.Atoi, GetBool — ParseBool, GetDuration — ParseDuration. Мусор в значении даст ноль без ошибки.
- **AllKeys и окружение** — AutomaticEnv не попадает в AllKeys и AllSettings: перечислить переменные процесса невозможно. Дамп показывает файл и дефолты; полную картину дают Get по ключам или BindEnv.
- **Пустая переменная** — По умолчанию пустое значение считается несуществующим: SERVER_PORT="" проиграет дефолту. Меняется AllowEmptyEnv(true), но включать стоит осознанно.

### Что дальше

Окружение подключено, и один бинарник принимает настройки от деплоя. Нерешённым остался конфликт имён: два сервиса на общей машине дерутся за PORT, а переменные соседних приложений просачиваются в ваш реестр. Следующий урок про SetEnvPrefix и BindEnv чинит обе беды: префикс превращает PORT в APP_PORT, а явная привязка ключей возвращает их в AllKeys и делает валидацию полной. Тема переменных окружения в CLI уже встречалась в [курсе Cobra](/courses/cobra/lessons/09-peremennye-okruzheniya/) — там os.Getenv и флаги; здесь всё сходится в один реестр с приоритетами. Потренируйтесь локальным Go: соберите секцию из урока, запустите с парой переменных и попробуйте предсказать дамп AllSettings до печати. Общий маршрут по темам — на [странице курса viper](/courses/viper/).

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

- AutomaticEnv включает проверку окружения на каждом чтении: ключ приводится к верхнему регистру, replacer меняет разделители, viper ищет переменную через os.LookupEnv.
- Без EnvKeyReplacer вложенный ключ ищется как SERVER.PORT — имя, которое shell выставить не может; replacer ставится до первых чтений.
- Окружение всегда строки: GetInt, GetBool и GetDuration приводят значения сами, молча давая нули на мусоре.
- env перекрывает конфиг-файл и дефолт, но уступает флагам и Set — порядок тот же, что и на общей лестнице приоритетов.
- AllKeys и AllSettings слепы к AutomaticEnv: дамп показывает файл и дефолты даже при работающих env-настройках.
- Пустая переменная по умолчанию считается несуществующей; AllowEmptyEnv меняет это поведение.
- Префикс и явная привязка BindEnv — следующий шаг: они убирают коллизии имён и возвращают env-ключи в дампы.

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

Добавьте в проект из прошлого урока SetEnvKeyReplacer и AutomaticEnv, задайте дефолт порта и запустите бинарник с SERVER_PORT=9090. Напечатайте GetInt и AllSettings рядом — убедитесь, что значение из окружения живёт только в Get. Затем добавьте IsSet-проверку обязательного API_KEY и попробуйте запустить без него.

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

### AutomaticEnv видит переменные без префикса — не опасно ли это?

Опасность без префикса другая: сервис может прочитать чужую переменную с тем же именем — например, чужой PORT. Префикс из следующего урока отделяет ваше пространство имён: APP_PORT ни с чем не конфликтует. Локально удобен голый режим, для деплоя добавляйте префикс.

### Почему AllSettings не показывает переменные окружения?

AllSettings строится от AllKeys, а тот собирает только зарегистрированные ключи: файл, дефолты, Set, BindEnv. Список переменных процесса огромен и к конфигурации относится лишь частично — viper не пытается угадать, какие из них ваши. Печатайте нужные ключи через Get или регистрируйте их BindEnv.

### Что вернёт Get, если переменная задана пустой строкой?

По умолчанию viper пропустит пустую переменную и возьмёт значение ниже по лестнице — из файла или дефолта. Логика такая: пустая строка чаще означает, что переменную забыли задать. Если пустота — легальное значение, включите AllowEmptyEnv(true) и обрабатывайте её валидацией на старте.
