Содержание урока · 10 разделов
- Альтернативы viper: когда комбайн избыточен
- envconfig: теги, дефолты, required
- koanf от knadh: лёгкая и модульная
- flag и env руками: путь CLI
- Подход → вес → когда
- Красные флаги: viper тянут зря
- Спор: viper как стандарт де-факто против осознанного выбора
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Альтернативы viper: когда комбайн избыточен#
Сервис читает пять переменных: порт, адрес базы, лог-уровень, таймаут, фича-флаг. Под ними стоит viper с реестром, лестницей приоритетов и watch-механикой — а работает из всего этого одна строка GetString. Это не обвинение viper: библиотека хороша там, где источников много, вопрос в другом — что вы платите за мощность, которую не используете: вес в go.mod, время сборки, час на обучение новичка, отдельную дисциплину диагностики реестра. Самый дешёвый момент для пересмотра — сейчас, пока конфиг спрятан за фасадом из урока про пакет config: наружу торчит Load и структура, и замена библиотеки сводится к правке одного файла. Если же вызовы Get размазаны по проекту, сначала фасад, потом переезд. Дальше четыре пути вниз, от нуля зависимостей до лёгкой библиотеки, и у каждого есть зона, где он честно лучший.
Нижняя ступень — os.Getenv, и у неё репутация хуже, чем она заслуживает. Для утилиты, CI-шага или сервиса с парой настроек это ровно то, что нужно: ноль зависимостей, ноль конфиг-файлов, значение берётся из окружения и живёт по правилам 12 factor. Две ловушки известны. Первая: os.Getenv не различает «переменной нет» и «переменная пустая» — обе дают пустую строку, и если пустота имеет смысл, нужна os.LookupEnv с bool-флагом. Вторая: приведение типов и дефолты пишутся руками — strconv.Atoi на каждом порту, константа-фолбэк рядом с местом чтения. Когда переменных становится десять, рука сама тянется к структуре, и тут появляется envconfig: он не умеет файлы и remote, зато превращает окружение в типизированную структуру с дефолтами и обязательными полями. По памяти: библиотека крошечная, одной зависимости, и держится на трёх тегах — envconfig, default, required.
envconfig: теги, дефолты, required#
envconfig работает через reflect: описываете структуру, вызываете Process с префиксом, библиотека раскладывает переменные по полям. Имя переменной берётся из тега envconfig, без тега — из имени поля; тег default подставляет значение, когда переменной нет; тег required делает отсутствие переменной ошибкой Process, а не пустым полем. По памяти, детали сверяйте с README: это маленькая библиотека, и её сила в том, что учить нечего — одна функция, одна структура. Обратите внимание, где проходят границы: файлы не читаются вообще, приоритет один — окружение, а значения вне структуры просто не попадают в конфиг. Для 12 factor-сервиса это не минус, а честный контракт: конфиг — окружение, точка. Ошибку Process не глотаем: она означает, что обязательное поле пусто, и сервис должен упасть до первого коннекта — fail fast в миниатюре.
Структура конфига на envconfig
GOtype Config struct {
Host string "envconfig:\"HOST\" default:\"db.local\""
Port int "envconfig:\"PORT\" default:\"5432\""
Token string "envconfig:\"TOKEN\" required:\"true\""
Debug bool "envconfig:\"DEBUG\""
}
func load() (Config, error) {
var c Config
if err := envconfig.Process("app", &c); err != nil {
return Config{}, err
}
return c, nil
}Теги показаны в двойных кавычках с экранированием: обратные кавычки в примерах курса заняты разметкой — в своём коде пишите теги привычными raw-строками Go. Process с префиксом app собирает APP_HOST и остальные переменные в структуру: тег меняет имя переменной, default держит значение на случай пустого окружения, required делает отсутствующий APP_TOKEN ошибкой, а не пустой строкой. Список читаемых настроек теперь виден в одном месте — структуре. Скопируйте пример и запустите локальным Go с парой переменных: поведение required проверяется быстрее, чем читается.
Тег default слишком удобен: рука пишет дефолт для пароля — и пароль попадает в исходники, в git-историю и в бинарник, откуда строки достаются обычным strings. Дефолт — место для портов, адресов и уровней логов, не для учётных данных. Токен либо required и приходит из окружения, либо не должен существовать. Проверьте свои структуры на предмет слова password рядом с default — в зрелых проектах такое находится чаще, чем кажется, и это не паранойя, а гигиена.
Проверка: required у envconfig
Переменная APP_TOKEN не задана, у поля стоит тег required. Что произойдёт при envconfig.Process?
koanf от knadh: лёгкая и модульная#
Спор viper vs koanf ведут обычно не о скорости, а об архитектуре. koanf — ответ на вопрос «а если источников всё-таки несколько?». По памяти о библиотеке: она лёгкая, без глобального состояния, собрана из провайдеров (откуда грузить — файл, окружение, карта) и парсеров (как разбирать — YAML, JSON, TOML). Инстанс создаётся явно — koanf.New с разделителем, каждый источник грузится отдельным Load, и никакого пакетного реестра, который виден всему проекту. Это главный сдвиг относительно viper: у viper глобальный v по умолчанию, у koanf глобальности нет совсем, и тест с двумя наборами настроек в одном процессе — обычное дело, а не борьба с реестром. Плата — церемония: источник и парсер собираются руками, префикс env и правило превращения APP_SERVER_HOST в server.host пишете колбэком вы. Детали API по памяти — сверяйте с README.
Два источника на koanf
GOk := koanf.New(".")
err := k.Load(file.Provider("config.yaml"), yaml.Parser{})
if err != nil {
log.Fatal(err)
}
err = k.Load(env.Provider("APP_", ".", func(s string) string {
return strings.Replace(
strings.ToLower(strings.TrimPrefix(s, "APP_")), "_", ".", -1,
)
}), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(k.String("server.host"), k.Int("server.port"))По памяти: сначала файл, потом env — значит, переменные окружения перекрывают значения из YAML, тот же принцип лестницы, что у viper, только порядок задаёте вы порядком Load. Колбэк в env.Provider превращает имя переменной в ключ: APP_SERVER_HOST уедет в server.host. Разделитель из koanf.New согласован с ключами — поменяйте точку на черту и перепишите адреса: библиотека не решает это за вас.
Проверка: архитектура koanf
Чем koanf отличается от viper архитектурнее всего?
Критерии выбора просты, если их честно посчитать. Первый — число источников: одно окружение? хватит envconfig. Файл плюс env с перекрытием? koanf или viper. Remote-хранилище и легаси-формат из 2010-го? вот тут viper разворачивается. Второй — hot-reload: если нужен WatchConfig с перечитыванием на лету, как в уроке про hot-reload, лёгкие альтернативы это умеют хуже или с оговорками; у koanf watch делается через провайдер файла, но это отдельная настройка, а не одна строка. Третий — вес зависимостей: viper тянет семейство библиотек spf13, и это видно в go.mod и во времени сборки; envconfig — один маленький пакет. Моё правило, за которое меня ругают за категоричность: список источников длиннее двух — думайте о viper или koanf; короче — берите envconfig и не оглядывайтесь. Право другого на иную оценку возникает там, где команда единая и шаблон один: единообразие иногда стоит лишней зависимости.
flag и env руками: путь CLI#
CLI-инструмент живёт по другим правилам: его конфиг — это флаги командной строки, и библиотека конфигурации там чаще всего лишняя. flag из stdlib объявляет флаг, дефолт и описание; Parse разбирает аргументы; значение из окружения подмешивается вручную — достали APP_ADDR, если пусто, взяли дефолт флага. Приоритет получается флаг выше env, env выше дефолта, и читается он прямо в коде, без реестра. Для десятка флагов этого достаточно; когда их становится тридцать-пятьдесят и появляются группы, общие флаги у подкоманд и конфиг-файл как документированная фича, посмотрите на Cobra: разбор связки viper с флагами был в уроке про флаги и Cobra, а приоритеты конфигурации в CLI-приложении — в уроке курса Cobra. Главное — не начинать с viper: добавить его потом легко, вырезать — уже работа.
Флаг с env-дефолтом
GOaddr := flag.String("addr", envDefault("APP_ADDR", ":8080"), "адрес слушателя")
verbose := flag.Bool("verbose", false, "подробный лог")
flag.Parse()
if *verbose {
fmt.Println("verbose: on")
}
func envDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}Флаг объявляется до flag.Parse, а дефолтом служит результат envDefault: переменная окружения подставилась, только если была непуста. Разыменование addr и verbose — после Parse, иначе получите нулевые значения. Соберите это в main и запустите локальным Go с ключами -addr и -verbose и без них: три прогона показывают весь приоритет флага над env и env над дефолтом без единой зависимости.
Что выведет программа?
Прочитайте код и выберите, что он выведет:
os.Setenv("APP_MODE", "")
mode := os.Getenv("APP_MODE")
if mode == "" {
fmt.Println("fallback")
} else {
fmt.Println(mode)
}Соберите загрузку envconfig
Допишите загрузку: инициализировать структуру, вызвать Process с префиксом и упасть на ошибке.
Соберите мини-CLI
Расставьте строки мини-CLI по порядку: объявить оба флага, разобрать аргументы, напечатать пометку при verbose.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Подход → вес → когда#
| Подход | Вес зависимостей | Когда брать |
|---|---|---|
| os.Getenv | ноль зависимостей | пара переменных, утилита, CI-шаг |
| flag + env руками | только stdlib | CLI-инструмент: конфиг — это аргументы команды |
| kelseyhightower/envconfig | одна маленькая библиотека | сервис 12 factor: конфиг целиком из окружения |
| koanf (knadh) | лёгкая модульная пара | файл + env + перекрытия без глобального реестра |
| viper | семейство библиотек spf13 | много источников, hot-reload, remote и легаси-форматы |
Красные флаги: viper тянут зря#
- В коде есть один GetString, а остальные источники не используются.
- config.yaml читается один раз и не меняется между окружениями: все различия — в env.
- Watch-механика включена, но на OnConfigChange не подписан никто.
- go.mod тянет семейство spf13 ради пяти переменных из окружения.
- Новый разработчик спрашивает, откуда берётся конфиг, и ответ занимает десять минут.
- Тесты с двумя наборами настроек в одном процессе ломают глобальный реестр.
Реальный случай из практики: CLI-инструмент с полусотней флагов тащил viper и читал три YAML-конфига — потому что «так было в шаблоне». Из всей машинерии пользователи использовали восемь флагов командной строки, а конфиги существовали в одном экземпляре у каждого. Замена на flag с подмешиванием env заняла вечер и убила три зависимости из go.mod. Граница у истории простая: если ваш CLI поддерживает конфиг-файл как документированную фичу — viper или koanf уместны; если флаги — это весь интерфейс, они и есть конфиг.
Карточки: альтернативы viper
Спор: viper как стандарт де-факто против осознанного выбора#
Аргумент за viper как стандарт: его знает каждый гофер, команды не спорят, шаблон проекта один, и viper в go.mod воспринимается как плата за единообразие, как линтер. Аргумент против: единообразие фиктивно, если реестр всё равно настраивают по-разному — префиксы, replacer, watch — и диагностика каждой настройки стоила отдельного урока этого курса. Моя позиция, за которую в командах любят спорить: выбор конфиг-библиотеки — техническое решение с ценой, а не данность, и маленький сервис на envconfig честнее сервиса на viper, где работает пять процентов возможностей. Граница спора такая: в большой команде с десятком сервисов и текучкой стандарт выигрывает — время людей дороже мегабайт; в маленькой команде или библиотеке с минимальным деревом зависимостей побеждает осознанный выбор, и лишняя зависимость там — реальная цена. Примените это к своему проекту: откройте go.mod и спросите, за что вы платите.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- viper избыточен, когда источников один-два: реестр, приоритеты и watch работают вхолостую.
- os.Getenv — нулевой вес, но пустую переменную он не отличает от отсутствующей: это работа LookupEnv.
- envconfig собирает окружение в структуру тремя тегами: envconfig для имени, default для фолбэка, required для ошибки.
- koanf — модульная библиотека без глобального состояния: инстанс явный, порядок Load задаёт приоритет источников.
- Для CLI конфиг — это флаги: flag плюс env руками закрывают вопрос без зависимостей.
- Критерии выбора: число источников, hot-reload, вес зависимостей — считайте их, а не следуйте привычке шаблона.
- Фасад config делает замену библиотеки дешёвой: миграция — правка одного пакета.
Домашнее задание
Возьмите сервис из урока про пакет config и перепишите загрузку на envconfig: структура с тегами, required для пароля, дефолты для порта. Затем сделайте то же на koanf с файлом и env. Сравните объём кода и состав go.mod и запишите критерий, при котором вернули бы viper.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли заменить viper на envconfig, если конфиг читается из файла?
Файлы envconfig не читает — это чисто окруженческая библиотека. Если файл нужен как источник, берите koanf или оставляйте viper. Компромисс: сгенерировать env-переменные из файла на уровне запуска, но такой трюк усложняет отладку и редко оправдан.
koanf действительно легче viper?
По памяти: дерево зависимостей заметно меньше, и отсутствие глобального реестра упрощает тесты. Скорость же чтения конфига — не заметная часть жизни сервиса: важнее вес сборки и ясность источников. Замеряйте на своём проекте, если критично; для большинства сервисов разница в микросекундах Get несущественна.
Что делать, если проект дорос до двух источников?
Сначала посчитайте реальные источники: часто файл и env — это один источник с перекрытием, и envconfig после генерации переменных хватает. Если файл нужен честно — koanf с двумя Load. Viper берите, когда добавляются remote, watch или формат, которого нет у лёгких библиотек.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context