Часто задаваемые вопросы
Базовое использование
Что выбрать — Load() или New()?
env.Load() (глобальный режим) подходит для простых приложений: одна загрузка, глобальное использование пакетных функций. Он автоматически применяет переменные к os.Environ.
env.New() (режим экземпляра) подходит для тестов и сценариев с несколькими конфигурациями: создаёт изолированный экземпляр, не применяет автоматически, требует явного Close().
// Простое приложение → глобальный режим
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. Это проектное решение: избегает случайного переопределения уже загруженной конфигурации во время выполнения.
// Первый вызов — успех
env.Load(".env")
// Второй вызов — возвращает ошибку
err := env.Load(".env.production")
// err == env.ErrAlreadyInitializedРешения:
// Решение 1: одна загрузка нескольких файлов (рекомендуется)
env.Load(".env", ".env.production")
// Решение 2: при необходимости реинициализации сначала сбросьте
env.ResetDefaultLoader() // В основном для тестов
env.Load(".env.production")Что происходит, если файл .env не существует?
Поведение по умолчанию: молча пропускает, без ошибки. Это обеспечивает гибкое развёртывание по принципу «есть — загружай, нет — игнорируй».
// DefaultConfig — при отсутствии файла молча пропускает
env.Load(".env", ".env.local")
// Даже если оба файла не существуют, ошибки не будетЕсли требуется ошибка при отсутствии файла (рекомендуется для продакшена):
cfg := env.ProductionConfig()
// FailOnMissingFile по умолчанию true (только для ProductionConfig)
loader, _ := env.New(cfg)Как получить доступ к вложенным значениям JSON/YAML?
Вложенные структуры JSON/YAML автоматически плоско преобразуются в имена ключей, разделённые подчёркиванием:
{
"database": {
"host": "localhost",
"port": 5432
}
}Хранится как: DATABASE_HOST=localhost, DATABASE_PORT=5432Три способа доступа эквивалентны:
host := env.GetString("DATABASE_HOST") // Плоский ключ (рекомендуется)
host := env.GetString("database.host") // Путь через точку
host := env.GetString("DATABASE.HOST") // Путь через точку в верхнем регистреТипы и обобщённость
Почему GetSlice — обобщённая функция, а не метод?
Go не поддерживает параметры типов для методов. GetSlice[T] должна быть функцией, а не методом:
// ❌ Как метод — ошибка компиляции (Go не поддерживает)
// loader.GetSlice[int]("PORTS")
// ✅ Как функция — работает
env.GetSliceFrom[int](loader, "PORTS")
// ✅ Пакетная функция
env.GetSlice[int]("PORTS")Как GetSlice разбирает значения среза?
Поиск по приоритету:
- Индексные ключи (рекомендуется):
KEY_0,KEY_1,KEY_2... - Разделение запятой:
KEY=val1,val2,val3
# Способ 1: индексные ключи
HOSTS_0=localhost
HOSTS_1=example.com
# Способ 2: разделение запятой
HOSTS=localhost,example.comhosts := env.GetSlice[string]("HOSTS") // ["localhost", "example.com"]Какие форматы логических значений поддерживаются?
GetBool без учёта регистра поддерживает следующие значения:
| Истина | Ложь |
|---|---|
true, 1, yes, on, enabled | false, 0, no, off, disabled |
Конкурентность и потокобезопасность
Можно ли одновременно вызывать Get из нескольких goroutine?
Да. Все методы Loader потокобезопасны. Библиотека использует сегментированные блокировки (sharded locks) для оптимизации производительности чтения/записи в высококонкурентных сценариях.
// Безопасный конкурентный доступ
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 переходит в режим деградации только для чтения после закрытия:
loader, _ := env.New()
defer loader.Close()
val := loader.GetString("KEY") // Нормальный возврат
// После Close()
val = loader.GetString("KEY") // Возвращает "" (нулевое значение)
err := loader.Set("KEY", "v") // Возвращает ErrClosedSecureValue
В чём разница между Release и Close?
| Метод | Обнуление памяти | Разблокировка памяти | Возврат в пул объектов |
|---|---|---|---|
Release() | ✅ | ✅ | ✅ |
Close() | ✅ | ✅ | ❌ |
Рекомендуется использовать Release(), который возвращает объект в пул, снижая давление на GC. Close() подходит для сценариев, где пулинг не требуется.
Автоматически ли обнуляется SecureValue при сборке мусора GC?
Да. SecureValue устанавливает финализатор, который автоматически обнуляет память при сборке мусора. Однако рекомендуется явно вызывать Release() или Close() для своевременной очистки, не полагаясь на недетерминированное время GC.
// ✅ Рекомендуется: явное освобождение
sv := env.GetSecure("API_KEY")
defer sv.Release()
// ⚠️ Полагаться на GC — не рекомендуется, но безопасно
sv := env.GetSecure("API_KEY")
// В конечном итоге будет обнулён GC, но время не определеноКак безопасно вести логирование?
Используйте Masked() или функции маскирования, никогда не выводите напрямую значение Reveal():
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 для фильтрации:
cfg := env.DefaultConfig()
cfg.Prefix = "MYAPP_" // Загружать только переменные с префиксом MYAPP_
loader, _ := env.New(cfg)
loader.LoadFiles(".env")# Содержимое файла .env
MYAPP_HOST=localhost # ✅ Загружено
MYAPP_PORT=8080 # ✅ Загружено
OTHER_KEY=value # ❌ Игнорируется (нет префикса MYAPP_)Как предотвратить переопределение конфигурации?
OverwriteExisting управляет переопределением существующих переменных:
// По умолчанию: без переопределения (безопасно)
cfg := env.DefaultConfig()
cfg.OverwriteExisting = false
// Среда разработки: разрешить переопределение
cfg := env.DevelopmentConfig()
// OverwriteExisting = trueКогда выполняется валидация RequiredKeys?
Только при явном вызове Validate(), не запускается автоматически при загрузке:
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:
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():
func TestGlobalMode(t *testing.T) {
// Очистка состояния предыдущего теста
env.ResetDefaultLoader()
defer env.ResetDefaultLoader()
env.Load(".env.test")
// Тестирование...
}Полное руководство по тестированию
Подробнее см. руководство Сценарии тестирования.
Связанная документация
- Быстрый старт — начало за 5 минут
- Шпаргалка — частые фрагменты кода
- Обработка ошибок — сигнальные ошибки и стратегии восстановления
- Формат файла — синтаксис .env/JSON/YAML