Skip to content

Часто задаваемые вопросы ​

Базовое использование ​

Что выбрать — Load() или New()? ​

env.Load() (глобальный режим) подходит для простых приложений: одна загрузка, глобальное использование пакетных функций. Он автоматически применяет переменные к os.Environ.

env.New() (режим экземпляра) подходит для тестов и сценариев с несколькими конфигурациями: создаёт изолированный экземпляр, не применяет автоматически, требует явного Close().

go
// Простое приложение → глобальный режим
env.Load(".env")
port := env.GetInt("PORT", 8080)

// Тесты / несколько конфигураций → режим экземпляра
loader, _ := env.New(env.TestingConfig())
defer loader.Close()
port := loader.GetInt("PORT", 8080)

Рекомендация по выбору

Если не уверены, начните с env.Load(). При возникновении потребности в изоляции тестов или нескольких конфигурациях переключайтесь на env.New().

Почему Load() можно вызвать только один раз? ​

Load() устанавливает глобальный загрузчик по умолчанию (паттерн singleton); повторный вызов возвращает ErrAlreadyInitialized. Это проектное решение: избегает случайного переопределения уже загруженной конфигурации во время выполнения.

go
// Первый вызов — успех
env.Load(".env")

// Второй вызов — возвращает ошибку
err := env.Load(".env.production")
// err == env.ErrAlreadyInitialized

Решения:

go
// Решение 1: одна загрузка нескольких файлов (рекомендуется)
env.Load(".env", ".env.production")

// Решение 2: при необходимости реинициализации сначала сбросьте
env.ResetDefaultLoader()  // В основном для тестов
env.Load(".env.production")

Что происходит, если файл .env не существует? ​

Поведение по умолчанию: молча пропускает, без ошибки. Это обеспечивает гибкое развёртывание по принципу «есть — загружай, нет — игнорируй».

go
// DefaultConfig — при отсутствии файла молча пропускает
env.Load(".env", ".env.local")
// Даже если оба файла не существуют, ошибки не будет

Если требуется ошибка при отсутствии файла (рекомендуется для продакшена):

go
cfg := env.ProductionConfig()
// FailOnMissingFile по умолчанию true (только для ProductionConfig)
loader, _ := env.New(cfg)

Как получить доступ к вложенным значениям JSON/YAML? ​

Вложенные структуры JSON/YAML автоматически плоско преобразуются в имена ключей, разделённые подчёркиванием:

json
{
  "database": {
    "host": "localhost",
    "port": 5432
  }
}
Хранится как: DATABASE_HOST=localhost, DATABASE_PORT=5432

Три способа доступа эквивалентны:

go
host := env.GetString("DATABASE_HOST")  // Плоский ключ (рекомендуется)
host := env.GetString("database.host")  // Путь через точку
host := env.GetString("DATABASE.HOST")  // Путь через точку в верхнем регистре

Типы и обобщённость ​

Почему GetSlice — обобщённая функция, а не метод? ​

Go не поддерживает параметры типов для методов. GetSlice[T] должна быть функцией, а не методом:

go
// ❌ Как метод — ошибка компиляции (Go не поддерживает)
// loader.GetSlice[int]("PORTS")

// ✅ Как функция — работает
env.GetSliceFrom[int](loader, "PORTS")

// ✅ Пакетная функция
env.GetSlice[int]("PORTS")

Как GetSlice разбирает значения среза? ​

Поиск по приоритету:

  1. Индексные ключи (рекомендуется): KEY_0, KEY_1, KEY_2...
  2. Разделение запятой: KEY=val1,val2,val3
bash
# Способ 1: индексные ключи
HOSTS_0=localhost
HOSTS_1=example.com

# Способ 2: разделение запятой
HOSTS=localhost,example.com
go
hosts := env.GetSlice[string]("HOSTS") // ["localhost", "example.com"]

Какие форматы логических значений поддерживаются? ​

GetBool без учёта регистра поддерживает следующие значения:

ИстинаЛожь
true, 1, yes, on, enabledfalse, 0, no, off, disabled

Конкурентность и потокобезопасность ​

Можно ли одновременно вызывать Get из нескольких goroutine? ​

Да. Все методы Loader потокобезопасны. Библиотека использует сегментированные блокировки (sharded locks) для оптимизации производительности чтения/записи в высококонкурентных сценариях.

go
// Безопасный конкурентный доступ
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        _ = env.GetString("KEY") // Потокобезопасно
    }()
}
wg.Wait()

Что произойдёт при вызове Get после Loader.Close()? ​

Возвращается нулевое значение без panic. Loader переходит в режим деградации только для чтения после закрытия:

go
loader, _ := env.New()
defer loader.Close()

val := loader.GetString("KEY") // Нормальный возврат

// После Close()
val = loader.GetString("KEY")  // Возвращает "" (нулевое значение)
err := loader.Set("KEY", "v")  // Возвращает ErrClosed

SecureValue ​

В чём разница между Release и Close? ​

МетодОбнуление памятиРазблокировка памятиВозврат в пул объектов
Release()✅✅✅
Close()✅✅❌

Рекомендуется использовать Release(), который возвращает объект в пул, снижая давление на GC. Close() подходит для сценариев, где пулинг не требуется.

Автоматически ли обнуляется SecureValue при сборке мусора GC? ​

Да. SecureValue устанавливает финализатор, который автоматически обнуляет память при сборке мусора. Однако рекомендуется явно вызывать Release() или Close() для своевременной очистки, не полагаясь на недетерминированное время GC.

go
// ✅ Рекомендуется: явное освобождение
sv := env.GetSecure("API_KEY")
defer sv.Release()

// ⚠️ Полагаться на GC — не рекомендуется, но безопасно
sv := env.GetSecure("API_KEY")
// В конечном итоге будет обнулён GC, но время не определено

Как безопасно вести логирование? ​

Используйте Masked() или функции маскирования, никогда не выводите напрямую значение Reveal():

go
sv := env.GetSecure("API_KEY")
defer sv.Release()

// ✅ Безопасно — маскированный вывод
log.Printf("API Key: %s", sv.Masked())    // [SECURE:32 bytes locked]
log.Printf("API Key: %s", sv)              // Аналогично (String() возвращает Masked())

// ✅ Безопасно — инструменты маскирования
masked := env.MaskValue("API_KEY", "sk-xxx") // sk-******************************
clean := env.SanitizeForLog(logMessage)       // Автообнаружение и маскирование

// ❌ Опасно — утечка открытого текста
plaintext := sv.Reveal()
log.Printf("API Key: %s", plaintext) // Не делайте так

Конфигурация и валидация ​

Как загружать только переменные с определённым префиксом? ​

Используйте поле Prefix для фильтрации:

go
cfg := env.DefaultConfig()
cfg.Prefix = "MYAPP_"  // Загружать только переменные с префиксом MYAPP_
loader, _ := env.New(cfg)
loader.LoadFiles(".env")
bash
# Содержимое файла .env
MYAPP_HOST=localhost    # ✅ Загружено
MYAPP_PORT=8080         # ✅ Загружено
OTHER_KEY=value         # ❌ Игнорируется (нет префикса MYAPP_)

Как предотвратить переопределение конфигурации? ​

OverwriteExisting управляет переопределением существующих переменных:

go
// По умолчанию: без переопределения (безопасно)
cfg := env.DefaultConfig()
cfg.OverwriteExisting = false

// Среда разработки: разрешить переопределение
cfg := env.DevelopmentConfig()
// OverwriteExisting = true

Когда выполняется валидация RequiredKeys? ​

Только при явном вызове Validate(), не запускается автоматически при загрузке:

go
cfg := env.ProductionConfig()
cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}
loader, _ := env.New(cfg)
loader.LoadFiles(".env")

// Явная валидация
if err := loader.Validate(); err != nil {
    if errors.Is(err, env.ErrMissingRequired) {
        log.Fatal("Отсутствуют обязательные переменные окружения")
    }
}

Тестирование ​

Как изолировать окружение в тестах? ​

Используйте TestingConfig() + независимый экземпляр Loader:

go
func TestConfig(t *testing.T) {
    cfg := env.TestingConfig()
    cfg.OverwriteExisting = true

    loader, err := env.New(cfg)
    if err != nil {
        t.Fatal(err)
    }
    defer loader.Close()

    // Каждый тест независим, не влияет на другие
    loader.Set("KEY", "test-value")
    val := loader.GetString("KEY")
    // Тестирование...
}

Как сбросить глобальный режим в тестах? ​

Используйте ResetDefaultLoader():

go
func TestGlobalMode(t *testing.T) {
    // Очистка состояния предыдущего теста
    env.ResetDefaultLoader()
    defer env.ResetDefaultLoader()

    env.Load(".env.test")
    // Тестирование...
}

Полное руководство по тестированию

Подробнее см. руководство Сценарии тестирования.

Безопасность и жизненный цикл ​

Переменные окружения процесса остаются после Close? ​

Да. Close() обнуляет только копии в памяти и не снимает переменные, ранее применённые к os.Environ (намеренное решение: окружение процесса уже могло быть унаследовано дочерними процессами, поэтому семантика отката ненадёжна). Чтобы удалить их, вызывайте Delete по каждому ключу до закрытия — снимаются только ключи, записанные этим loader.

Как запретить файлу конфигурации читать секреты процесса? ​

Когда файл конфигурации приходит из недоверенного источника (загрузки пользователей, внешняя доставка), область развёртки по умолчанию позволяет ${VAR} откатываться к окружению процесса, рискуя захватом секретов в значения переменных. Включите файловую область, чтобы заблокировать это:

go
cfg := env.DefaultConfig()
cfg.ExpansionScope = env.ExpansionFileOnly // ${VAR} разрешает только переменные файла

См. Развёртка переменных · Область развёртки.

Почему $ в значении изменился при обратном чтении? ​

При включённой по умолчанию развёртке последовательности $VAR/${VAR} разворачиваются при загрузке. Если значение содержит буквальные знаки доллара (цены, строки шаблонов), загружайте с cfg.ExpandVariables = false либо см. Сериализация · Подводные камни round-trip об аналогичной проблеме при обратном чтении вывода Marshal.

Связанная документация ​