Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~16 мин +20 XP

Viper и альтернативы: env, koanf и stdlib

Альтернативы viper нужны, когда ваш сервис читает пять переменных из окружения, а реестр, лестница приоритетов и watch-механика работают вхолостую. Разберём четыре пути вниз: чистый os.Getenv, envconfig с тегами и дефолтами, koanf от knadh без глобального состояния, flag из stdlib для CLI — плюс критерии выбора и таблица «вес — когда».

Содержание урока · 10 разделов

Альтернативы 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

GO
type 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 проверяется быстрее, чем читается.

Проверка: 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

GO
k := 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-дефолтом

GO
addr := 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 с префиксом и упасть на ошибке.

var cfg Config err := envconfig.("app", &cfg) if err != nil { log.(err) } fmt.Println(cfg.Host)

Соберите мини-CLI

Расставьте строки мини-CLI по порядку: объявить оба флага, разобрать аргументы, напечатать пометку при verbose.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Подход → вес → когда#

ПодходВес зависимостейКогда брать
os.Getenvноль зависимостейпара переменных, утилита, CI-шаг
flag + env рукамитолько stdlibCLI-инструмент: конфиг — это аргументы команды
kelseyhightower/envconfigодна маленькая библиотекасервис 12 factor: конфиг целиком из окружения
koanf (knadh)лёгкая модульная парафайл + env + перекрытия без глобального реестра
viperсемейство библиотек spf13много источников, hot-reload, remote и легаси-форматы

Красные флаги: viper тянут зря#

  • В коде есть один GetString, а остальные источники не используются.
  • config.yaml читается один раз и не меняется между окружениями: все различия — в env.
  • Watch-механика включена, но на OnConfigChange не подписан никто.
  • go.mod тянет семейство spf13 ради пяти переменных из окружения.
  • Новый разработчик спрашивает, откуда берётся конфиг, и ответ занимает десять минут.
  • Тесты с двумя наборами настроек в одном процессе ломают глобальный реестр.

Карточки: альтернативы viper

Термин

os.Getenv vs os.LookupEnv

нажмите, чтобы перевернуть
Определение

Getenv возвращает пустую строку и для отсутствующей переменной, и для пустой — различить их нельзя. LookupEnv возвращает значение и bool, поэтому пустота как осознанное значение ловится только им.

1 / 6

Спор: 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 · Viper и альтернативы: env, koanf и stdlib)

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

Можно ли заменить viper на envconfig, если конфиг читается из файла?

Файлы envconfig не читает — это чисто окруженческая библиотека. Если файл нужен как источник, берите koanf или оставляйте viper. Компромисс: сгенерировать env-переменные из файла на уровне запуска, но такой трюк усложняет отладку и редко оправдан.

koanf действительно легче viper?

По памяти: дерево зависимостей заметно меньше, и отсутствие глобального реестра упрощает тесты. Скорость же чтения конфига — не заметная часть жизни сервиса: важнее вес сборки и ясность источников. Замеряйте на своём проекте, если критично; для большинства сервисов разница в микросекундах Get несущественна.

Что делать, если проект дорос до двух источников?

Сначала посчитайте реальные источники: часто файл и env — это один источник с перекрытием, и envconfig после генерации переменных хватает. Если файл нужен честно — koanf с двумя Load. Viper берите, когда добавляются remote, watch или формат, которого нет у лёгких библиотек.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Chi25 уроков~9 чпрактика25 уроков уже доступны

Chi: роутер для Go HTTP-сервисов

Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.

  • Строить маршруты chi: методы, параметры пути, wildcards
  • Писать middleware и понимать порядок их выполнения
  • Группировать маршруты: Route, Group и подроутеры
  • Прокидывать данные в хендлеры через context.Context