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")
    // Тестирование...
}

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

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

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