Содержание урока · 11 разделов
- viper RegisterAlias: как устроен синоним
- Сценарии: короткие имена, совместимость, миграция
- Реестр имён: ключ → алиас → кто пользуется
- Миграция схемы конфига: план из трёх шагов
- Границы алиаса: окружение, дампы, Unmarshal
- Гигиена алиасов
- Спор: алиасы навсегда или config_version
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Релиз 2.0 ушёл в четверг, а в пятницу открылись тикеты: сервисы пользователей поднялись, но подключились не к своим базам, а к localhost. Причина оказалась скучной: мы переименовали ключ конфига из db_host в database.host, а старые config.yaml у клиентов остались с db_host. Viper честно прочитал файл целиком — значение под старым именем лежало в реестре, — но код спрашивал новое имя, не находил его и брал дефолт. Молча, без единой ошибки, сотни конфигов за один вечер. Средство от этого — viper RegisterAlias: вызов объявляет старое имя синонимом нового, и оба имени читают и пишут одно значение. В этом уроке разбираем механику алиасов, план миграции схемы конфига, связку с окружением и две детали, которые надо знать до первого переименования: порядок аргументов и невозможность снять алиас.
viper RegisterAlias: как устроен синоним#
RegisterAlias принимает два аргумента и складывает их в отдельную карту реестра: первый — алиас, второй — реальный ключ. После вызова viper.RegisterAlias("db_host", "database.host") имя db_host становится вторым именем database.host. Механика проявляется на каждом чтении: Get, GetString, GetInt и IsSet сперва разворачивают запрошенный ключ через карту алиасов и только потом спускаются по лестнице источников — Set, флаги, окружение, файл, дефолты. Поэтому через алиас видно ровно то же, что и через оригинал: если значение пришло из переменной окружения, алиас вернёт значение из окружения; если из файла — из файла. Запись тоже общая: Set через алиас попадает в реальный ключ, и значение сразу видно под обоими именами. Отсюда практический вывод: алиас — не копия значения, а синоним в реестре. Дубликата нет, синхронизировать нечего, а однонаправленность сопоставления означает лишь направление разворачивания — из синонима в ключ. Обратная ссылка не нужна: оба имени и так ведут в одно место.
Первый алиас: чтение и запись через оба имени
GOviper.SetDefault("database.host", "localhost")
viper.RegisterAlias("db_host", "database.host")
fmt.Println(viper.GetString("db_host")) // localhost — из реального ключа
fmt.Println(viper.GetString("database.host")) // localhost — то же значение
viper.Set("db_host", "db.internal:5432") // запись через алиас
fmt.Println(viper.GetString("database.host")) // db.internal:5432Значение живёт под реальным ключом database.host, а db_host работает как указатель на него. Чтение через алиас вернуло дефолт, запись через алиас легла в реальный ключ — и соседняя строка это показала. IsSet и GetInt ведут себя так же: разворачивание происходит до обращения к источникам.
Сценарии: короткие имена, совместимость, миграция#
Три сценария покрывают почти всё, зачем алиасы вообще нужны. Первый — миграция схемы: ключ переименован, старое имя остаётся рабочим синонимом, пока пользователи переносят свои конфиги; этому сценарию посвящён план ниже. Второй — совместимость с прошлой версией публичного инструмента: если ваш CLI держит у людей config.yaml, старые имена приходится поддерживать дольше, чем хотелось бы, и алиас дешевле ветвлений на каждом чтении. Третий — короткие имена: RegisterAlias("port", "server.port") позволяет писать в конфиг-файле port: 8080 вместо вложенной секции — удобно для примеров документации и быстрых правок руками. С короткими переменными окружения алиас не поможет: поиск по env идёт по развёрнутому ключу, и сокращение имени в реестре не меняет имя переменной — это работа для BindEnv с явным именем. Реестр имён из таблицы ниже стоит держать рядом со схемой конфига: без него через год никто не вспомнит, какое имя основное, а какое синоним.
Реестр имён: ключ → алиас → кто пользуется#
| Реальный ключ | Алиас | Кто пользуется |
|---|---|---|
| database.host | db_host | пользователи версии 1.x со старыми config.yaml |
| server.port | port | короткие имена в примерах документации |
| log.level | loglevel | старые деплой-скрипты и манифесты |
| api.timeout | timeout | внутренние команды поддержки |
Миграция схемы конфига: план из трёх шагов#
Задача миграции: новое имя становится основным, старое остаётся синонимом, и никто не едет в дефолты. Шаг один — новое имя получает SetDefault, и весь ваш код переходит на database.host. Шаг два — до регистрации алиаса и до включения окружения значение переносится со старого имени: если чужой config.yaml содержит db_host, оно копируется на новое имя через Set. Без этого шага алиас бессилен: он разворачивает чтение алиаса в database.host, а значение из чужого файла лежит под db_host, куда код теперь не заглядывает — тот самый молчаливый дефолт из начала урока. Шаг три — RegisterAlias: с этого момента старое имя читается и пишется через новое, а данные, которые пользователи сами переложат на новое имя, живут обычной жизнью. Если старое значение могло приходить и из переменных окружения, перед переносом проверьте, что переменная пуста: Set стоит выше env на лестнице приоритетов и начнёт её глушить.
Миграция: старое имя живёт, переменная деплоя тоже
GOviper.SetDefault("database.host", "localhost")
viper.ReadInConfig() // чужие config.yaml уже в реестре
// старое имя из файла переносим на новое (env ещё не включён)
if viper.IsSet("db_host") && os.Getenv("DB_HOST") == "" {
viper.Set("database.host", viper.Get("db_host"))
}
viper.RegisterAlias("db_host", "database.host") // синоним навсегда
viper.BindEnv("database.host", "DB_HOST") // старая переменная деплоя
fmt.Println(viper.GetString("db_host")) // файл, DB_HOST или localhostПорядок строк критичен: перенос выполняется, пока db_host ещё не алиас и AutomaticEnv не включена, поэтому IsSet честно смотрит в файл. После RegisterAlias чтение через старое имя достаёт перенесённое значение, а BindEnv направляет на новый ключ старую переменную деплоя.
Границы алиаса: окружение, дампы, Unmarshal#
RegisterAlias живёт внутри реестра и не создаёт привязок к источникам. Привязка к окружению у ключа своя: SetEnvPrefix и BindEnv не узнают о синонимах. Поиск по переменным viper ведёт уже по развёрнутому имени: алиас db_host превращается в database.host, и AutomaticEnv проверяет DATABASE_HOST. Отсюда два следствия. Первое: старую переменную деплоя привязывайте к реальному ключу явно — BindEnv("database.host", "DB_HOST"). Второе: привязка, сделанная под именем алиаса, после регистрации синонима перестаёт работать — viper консультирует её только при чтении по развёрнутому имени. С дампами та же история вдобавок к приятному: алиасы попадают в AllKeys, и AllSettings покажет db_host рядом с database.host с одним значением. Unmarshal получит оба поля — mapstructure возьмёт то, что названо в тегах структуры, лишнее проигнорирует. А вот WriteConfig запишет оба имени в файл: при генерации конфигов для пользователей двойников придётся чистить.
Оба имени в дампе реестра
GOviper.SetDefault("database.host", "localhost")
viper.RegisterAlias("db_host", "database.host")
fmt.Println(viper.AllKeys())
// [database.host db_host] — алиас перечислен рядом с реальным ключом
fmt.Println(viper.GetString("db_host"), viper.GetString("database.host"))
// localhost localhost — значение одно, имён дваAllKeys собирает имена из карты алиасов наравне с файлом и дефолтами, поэтому в дампе появляются двойники. Для логирования конфигурации это шум, для поиска забытых синонимов — подсказка. Проверьте локальным Go: два Println рядом с регистрацией алиаса дают полную картину за минуту.
RegisterAlias("database.host", "db_host") регистрирует database.host как синоним db_host — ключа, у которого значений нет. Чтение по обоим именам теперь резолвится в пустоту: по database.host — потому что он разворачивается в db_host, по db_host — потому что под ним ничего нет. Ошибки при этом нет никакой: оба Get вернут нулевые значения, и вы узнаете об этом по продакшену. Проверка IsSet по обоим именам сразу после ReadInConfig ловит переворот за секунды.
В публичном API нет UnregisterAlias: зарегистрированный синоним живёт до конца процесса, и перерегистрировать его на другой ключ тоже нельзя. Поэтому имена проектируют до релиза, а не после. В тестах выход есть — собственный реестр через viper.New(), где никаких алиасов ещё не зарегистрировано; в живом процессе — только перезапуск с новой версией кода.
Гигиена алиасов#
- Первый аргумент RegisterAlias — синоним, второй — реальный ключ со значениями; перепутанный порядок слепит оба имени.
- Алиас регистрируется рядом с SetDefault и до первых чтений: старое имя обязано работать с самого первого Get.
- Значение из чужих старых конфигов переносится на новое имя до регистрации алиаса — сам по себе алиас данные не двигает.
- В своём коде читайте и пишите только новое имя: алиас существует для внешних конфигов, а не для вас.
- Алиас не создаёт привязок к окружению и флагам: BindEnv и BindPFlag настраиваются на реальный ключ отдельно.
- AllKeys и AllSettings показывают оба имени, WriteConfig записывает двойников в файл — при генерации конфигов чистите лишнее.
Проверка: чтение через алиас
Выполнены viper.SetDefault("database.host", "localhost") и viper.RegisterAlias("db_host", "database.host"). Что вернёт viper.GetString("db_host")?
Проверка: снятие алиаса
Алиас db_host больше не нужен: все пользователи переехали на новое имя. Каким вызовом его снять?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
viper.SetDefault("database.host", "localhost")
viper.RegisterAlias("db_host", "database.host")
viper.Set("db_host", "db.internal:5432")
fmt.Println(viper.GetString("database.host"))Соберите миграцию ключа
Допишите три вызова: дефолт нового имени, регистрацию синонима и привязку старой переменной деплоя.
Соберите секцию миграции
Расставьте строки: дефолт, алиас, привязка переменной и два чтения — до и после записи через старое имя.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: алиасы ключей
Спор: алиасы навсегда или config_version#
Моё отношение к алиасам двойственное, и на code review я его формулирую так: алиасы — отличное средство разовой миграции и плохой способ постоянного дизайна. Как склейка после переименования они идеальны: пара строк кода, ноль сломанных пользователей. Но без присмотра синонимы превращаются в свалку: имена плодятся с каждым релизом, дампы двоятся, и через два года команда не помнит, какое из четырёх имён ключа основное. Альтернатива для проектов, где схема конфига меняется регулярно, — версионирование: ключ config_version в начале файла и явный мигратор, который при чтении поднимает конфиг с первой версии до текущей, и каждый шаг перехода виден в истории кода. Плата — инфраструктура: список версий, переходники, тесты на миграции. Критерий простой: один-два переименованных ключа — алиасы и строка в changelog; схема движется каждые пару релизов или конфиги генерируют внешние системы — config_version с мигратором. Правы бывают оба подхода; ошибка — не выбрать ни один и копить синонимы молча.
Что дальше#
Алиасы закрыли переименования: схема конфига двигается, а старые имена продолжают работать. Следующий урок — про профили окружений: один бинарник в dev, staging и prod, файлы config.dev.yaml и config.prod.yaml, выбор контура через флаг и APP_ENV — и правило, по которому секреты в файлах не живут вовсе. Полный маршрут по темам — на странице курса viper. Потрогайте миграцию локальным Go: соберите секцию из урока, положите рядом config со старым именем и проверьте, что чтение через оба имени возвращает одно значение до и после Set через алиас.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- RegisterAlias(alias, key) объявляет синоним: первый аргумент — новое имя, второй — реальный ключ; чтение, запись и IsSet работают через оба имени.
- Алиас разворачивается в реальный ключ до поиска по источникам, поэтому через оба имени видны одни и те же значения из Set, env, файла и дефолтов.
- Значение из чужих старых конфигов переносится на новое имя до регистрации алиаса: сам по себе алиас данные не двигает.
- Алиас не создаёт привязку к окружению: старую переменную деплоя привязывайте к реальному ключу явно — BindEnv("database.host", "DB_HOST").
- AllKeys и AllSettings показывают оба имени, а WriteConfig записывает двойников в файл — при генерации конфигов их чистят.
- Снять алиас нельзя: публичного UnregisterAlias нет, синоним живёт до конца процесса; в тестах — отдельный реестр viper.New().
- Для пары переименованных ключей алиасы дешевле всего; при регулярной смене схемы вводите config_version и явный мигратор.
Домашнее задание
Возьмите конфиг с ключом server.port: переименуйте его в http.port, добавьте RegisterAlias для старого имени и BindEnv для HTTP_PORT. Проверьте чтение через оба имени, запись через алиас и напечатайте AllKeys — убедитесь, что двойники видны. Затем попробуйте снять алиас и посмотрите, какие варианты вам доступны.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем алиас отличается от Set с тем же значением?
Set копирует значение в реестр в момент вызова: дальше две копии живут своей жизнью, и после правки одной вторая устаревает. Алиас копии не создаёт — оба имени читают один и тот же источник, синхронизировать нечего.
Работает ли алиас для вложенных ключей?
Да: RegisterAlias("dbh", "database.host") даёт короткое имя вложенному ключу, разворачивание происходит на каждом чтении. Пригодится и для Unmarshal — только помните, что в итоговую карту попадут оба поля, а не одно.
Можно ли зарегистрировать несколько алиасов на один ключ?
Да: каждый вызов RegisterAlias добавляет ещё одно имя, и все они разворачиваются в один реальный ключ. Проблема не в количестве, а в забвении: держите реестр имён из этого урока рядом со схемой конфига и объявляйте срок жизни синонимов в changelog.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Профили dev, staging и prod
- Секреты: почему пароли не живут в YAML
- Пакет config: оборачиваем viper в свой API
- Валидация конфигурации до запуска
- Sub(): разбираем конфиг на поддеревья
- WriteConfig и SafeWriteConfig: сохраняем состояние
- WatchConfig и OnConfigChange: живой конфиг
- Вложенные ключи: деревья и mapstructure
- Все курсы по Go
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context