Содержание урока · 10 разделов
Второй урок курса закончился на ReadInConfig: файл config.yaml найден и прочитан, значения поехали в программу. Ровно здесь команды обычно затыкаются: за пределами туториалов конфиги живут не только в YAML — инфраструктура соседнего отдела хранит настройки в TOML, генератор деплоя выплёвывает JSON, а легаси-сервис тянет за собой ini. Переписывать код чтения под каждый формат не станет никто, и правильно: viper читает всё одним API, парсер выбирается по расширению файла, а программа не меняется ни на символ. В этом уроке — полный список форматов, поиск файла без расширения, фиксированный порядок перебора расширений и чтение конфига прямо из строки или потока. Плюс одна ловушка, на которой команды теряют недели: что победит, если в папке лежат два конфига разных форматов.
Форматы конфигов go: что читает viper из коробки#
Список длиннее, чем ожидают: yaml и yml, json, toml, hcl — формат HashiCorp, ini, properties с вариантами props и prop, javaproperties, dotenv и env, tfvars из мира Terraform. Отдельно стоит env: переменные окружения приходят не из файла, а из окружения процесса и подключаются через AutomaticEnv — о ней в курсе свой урок. Все файловые форматы устроены одинаково: на ReadInConfig viper определяет формат и вызывает соответствующий парсер, а распарсенное дерево целиком ложится в единый реестр ключей. Дальше источнику без разницы: GetInt и GetString работают с toml-конфигом ровно так же, как с yaml, и семейство Get об этом даже не узнает. Отдельные парсеры подключать не нужно — всё уже внутри библиотеки. Переключение формата — это замена файла и, в отдельных случаях, одной строки настройки, но не правка кода чтения.
Поиск файла без расширения
GOviper.SetConfigName("config") // без расширения!
viper.AddConfigPath("./configs")
viper.AddConfigPath(".") // запасной вариант: текущая папка
err := viper.ReadInConfig()
if err != nil {
log.Fatal("конфиг не найден:", err)
}SetConfigName задаёт имя без расширения — расширение viper подберёт сам. Пути добавляются в порядке приоритета: сначала ./configs, потом текущая папка. ReadInConfig перебирает каталоги, а внутри каждого — поддерживаемые расширения, и берёт первый найденный файл. Ошибка возвращается только когда ни один путь ничего не дал.
Порядок SupportedExts: viper выбирает файл сам#
Раз расширение подбирается автоматически, напрашивается вопрос: а в каком порядке перебираются варианты? Порядок фиксированный и зашит в переменную viper.SupportedExts: json, toml, yaml, yml, properties, props, prop, hcl, tfvars, dotenv, env, ini. Логика поиска простая: в каждом каталоге из AddConfigPath viper идёт по этому списку слева направо и берёт первый существующий файл. Отсюда два следствия. Первое: в одной папке json всегда сильнее toml, а toml — сильнее yaml, независимо от того, какой файл вы считали главным. Второе: порядок через API не настраивается — ни переставить расширения, ни отдать приоритет yaml нельзя; единственный способ зафиксировать выбор жёстко — задать файл явно через SetConfigFile, который поиск отключает вовсе. Для парсера форматы равноправны, но не для поиска: разница между «поддерживается» и «будет найдено первым» здесь принципиальная, и о ней следующая врезка.
В configs/ лежит config.yaml, туда же падает config.json — сгенерировал инструмент деплоя или коллега положил «на пробу». Viper молча берёт json: он стоит в SupportedExts раньше. Программа работает, но правки в yaml не применяются — и вы неделю проверяете отступы, кавычки и кеш, пока не заметите второй файл. Лечение: один формат на каталог либо SetConfigFile с точным путём.
Тот же код читает TOML
GOpackage main
import (
"fmt"
"log"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigFile("configs/config.toml")
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
fmt.Println(viper.GetString("server.host"), viper.GetInt("server.port"))
}configs/config.toml здесь содержит секцию [server] со строками host = "0.0.0.0" и port = 8080. Формат определяется по расширению toml — никаких SetConfigType не нужно. Замените файл на config.yaml с тем же деревом — код не изменится ни на символ: парсер другой, реестр тот же. Вложенные ключи читаются через точку: server.port — это port внутри секции server.
SetConfigType и ReadConfig: конфиг не из файла#
Файл — не единственный источник. Строка с настройками может прийти из переменной окружения, из stdin, из расшифрованного секрета или из тестовой фикстуры — viper умеет парсить любой io.Reader через ReadConfig. Но у потока нет расширения: откуда парсеру знать формат? Для этого существует SetConfigType — она задаёт формат явно и обязательна перед ReadConfig: без неё вернётся ошибка Unsupported Config Type, viper содержимое не угадывает. Та же настройка спасает для файлов без расширения: SetConfigFile("configs/config") плюс SetConfigType("yaml") читается нормально. И честная оговорка по курсу: чтение файлов и потоков — это файловый ввод-вывод, которого в браузерной песочнице нет; запускайте примеры этого урока локальным Go, где папка configs лежит рядом с main.go.
JSON из строки
GOpayload := []byte("{"app": {"name": "billing", "port": 8080}}")
viper.SetConfigType("json")
if err := viper.ReadConfig(bytes.NewReader(payload)); err != nil {
log.Fatal(err)
}
fmt.Println(viper.GetString("app.name"), viper.GetInt("app.port"))Порядок обязателен: сначала SetConfigType("json"), затем ReadConfig с любым io.Reader — bytes.NewReader подходит идеально. Вложенность json превращается в привычные ключи с точками: app.name. Перепишите ту же структуру в yaml и поменяйте тип — вызовы чтения останутся прежними. На этом приёме строятся тесты: фикстура в памяти вместо файла на диске.
Какой формат конфига выбрать#
Критерий не «какой круче», а кто и как правит файл. yaml — выбор по умолчанию для читаемых сервисных конфигов: отступы, комментарии, якоря для повторяющихся кусков; файл редактируют люди руками. json — когда конфиг читает не человек, а машина: генерируется деплоем, отдаётся между сервисами, диффается инструментами; комментариев нет, и для машинного обмена это честно. toml — территория rust-экосистемы и cargo-подобных проектов: если в команде есть рустовщики или репозиторий к ним тяготеет, toml снимает вечный спор о табах и пробелах — там таблицы вместо отступов. ini — для legacy: старые сервисы, настройки Windows-приложений, всё, что уже существует и его жалко переписывать; вложенность в ini бедная, и тащить туда новые проекты я бы не стал. properties и dotenv — для контейнеров и пар вида KEY=value. И спорный совет напоследок: держите один формат на проект, даже когда viper позволяет смешивать — два формата удваивают число мест, где можно промахнуться.
Ниши форматов#
| Формат | Сильная сторона | Типичное место | Нюанс |
|---|---|---|---|
| yaml | отступы, комментарии, читаемость | сервисные конфиги, правит человек | чувствителен к отступам; json в поиске сильнее |
| json | машинный обмен, парсер в любой среде | сгенерированные конфиги, обмен сервисов | нет комментариев, диффы шумные |
| toml | таблицы вместо отступов, строгий синтаксис | rust-проекты, cargo-стиль | массивы таблиц поначалу путают |
| ini | простота, наследие | legacy-сервисы, настройки Windows | плоская вложенность, всё — строки |
| properties, dotenv | пары KEY=value | env-файлы деплоя, java-наследие | типов и вложенности нет |
ReadInConfig возвращает разные ошибки для разных провалов, и различать их стоит с первого дня. Когда ни один путь не дал файла, прилетает viper.ConfigFileNotFoundError — тип, который проверяют через errors.As. Это штатная ситуация, а не авария: программа может продолжить жизнь на значениях по умолчанию, и как готовить её к конфигу-которого-нет, разобрано в уроке про SetDefault. Совсем другое дело — ошибка парсинга: файл нашёлся, но синтаксис битый — незакрытая кавычка, таб вместо пробелов в yaml. Конфиг при этом не загружается целиком: viper не читает «половину файла», и это правильное поведение — запуск с полузагруженными настройками хуже падения на старте. Сообщение называет файл и причину, но не строку — blame по недавнему коммиту быстрее гаданий. Третьей категории, «файл есть, но ключа в нём нет», для ReadInConfig не существует: отсутствующий ключ — не ошибка чтения, а вопрос к тому, кто читает значения, и об этом следующий урок.
Признаки, что формат выбран плохо#
- Диффы конфига в ревью нечитабельны: одна правка порождает десять строк шума — формат не для человека.
- Каждый новый ключ приходится объяснять коллегам вслух: комментариев нет, а это json, который правят руками.
- Три уровня вложенности, и половина багов — забытый отступ: глубокие деревья в ini и properties не живут.
- Одна и та же настройка описана в двух файлах разных форматов, и никто не помнит, какой победит, — см. порядок SupportedExts.
- Конфиг генерируется скриптом, а правится руками, и каждый прогон сносит комментарии: машинный и человеческий форматы смешаны в одном файле.
Мой рабочий принцип: yaml — людям, json — машинам, и не смешивать в одном каталоге. Спорная половина — «никогда не смешивать»: в монорепо машинные json-файлы живут рядом с рукописными yaml, и это нормально, пока пути поиска разведены по разным папкам. Граница простая: если два файла попадают в один AddConfigPath — оставляйте один формат.
Проверка: какой файл победит
В папке ./configs лежат три файла: config.yaml, config.json и config.toml. Задан SetConfigName("config"), SetConfigFile не задан. Какой файл прочитает ReadInConfig?
Проверка: ReadConfig без типа
Вы вызываете viper.ReadConfig(bytes.NewReader(data)) с корректным JSON, но забыли SetConfigType. Что произойдёт?
Что выведет чтение из строки?
Прочитайте код и выберите, что он выведет:
payload := []byte("{"app": {"name": "billing", "port": 8080}}")
viper.SetConfigType("json")
viper.ReadConfig(bytes.NewReader(payload))
fmt.Println(viper.GetString("app.name"), viper.GetInt("app.port"))Соберите поиск конфига
Допишите: имя без расширения, путь поиска, подсказку формата для файла без расширения и сам вызов чтения.
Соберите загрузку конфига по строкам
Расставьте строки: имя конфига, два пути поиска, подсказка формата, чтение с обработкой ошибки и вывод значения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: форматы и поиск
Что дальше#
Форматы разобраны: файл найден, реестр заполнен — осталось вынуть из него значения, и здесь начинается самое интересное. Семейство Get с приведением типов, нулевыми значениями вместо ошибок и ловушкой any — тема следующего урока о чтении значений. Примеры запускаются локальным Go, и один опыт стоит сделать обязательно: положите в configs/ конфиг одного формата, затем рядом второго и по дампу настроек убедитесь, кто победил — двухминутная демонстрация порядка SupportedExts запоминается лучше любого текста. Полный маршрут по курсу — на странице курса viper.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- viper читает yaml, json, toml, hcl, ini, properties, javaproperties, dotenv и tfvars одним API: парсер выбирается по расширению, код чтения не меняется.
- Поиск без SetConfigFile идёт по путям из AddConfigPath в порядке добавления, а внутри каталога — по SupportedExts: json сильнее toml, toml сильнее yaml.
- Два конфига разных форматов в одной папке — источник недельной отладки: viper берёт первый по списку и не предупреждает.
- SetConfigFile задаёт файл явно и отключает поиск; для файла без расширения добавьте SetConfigType.
- ReadConfig принимает любой io.Reader — строку, stdin, расшифрованный секрет; без SetConfigType вернётся Unsupported Config Type.
- ConfigFileNotFoundError — штатная ситуация для программы на дефолтах; ошибка парсинга валит запуск целиком, и это правильное поведение.
- Выбор формата — вопрос редактора: yaml для людей, json для машин, toml в cargo-стиле, ini для legacy; один формат на каталог — минимум сюрпризов.
Домашнее задание
Соберите проект с папкой configs и загрузкой через SetConfigName. Положите туда config.yaml, затем рядом config.json с другим значением порта. Запустите программу и по дампу настроек убедитесь, что победил json. После этого переключите загрузку на SetConfigFile и повторите прогон — теперь формат за вами, а не за порядком расширений.
Личные заметкиПусто
Вопросы и ответы по уроку
Какие форматы viper поддерживает по умолчанию — парсеры подключать отдельно не нужно?
Все базовые форматы — yaml, json, toml, hcl, ini, properties, javaproperties, dotenv, env, tfvars — уже внутри библиотеки, тянуть зависимости не требуется. Формат выбирается по расширению файла или через SetConfigType. Удалённые хранилища вроде etcd и Consul — единственный случай с особыми билд-тегами, но это тема одного из поздних уроков курса.
Можно ли заставить viper предпочесть yaml, если рядом лежит json?
Порядок SupportedExts через API не настраивается: json проверяется раньше yaml всегда. Рабочие варианты: убрать конфликтующий файл, разложить файлы по разным каталогам с отдельными AddConfigPath или задать точный путь через SetConfigFile — тогда поиск не участвует вовсе.
ReadInConfig вернул ошибку — файл не нашёлся или он битый?
Проверяйте тип через errors.As: viper.ConfigFileNotFoundError означает, что поиск прошёл впустую, и программа может стартовать на дефолтах. Любая другая ошибка — парсинг: файл найден, синтаксис битый, конфиг не загружен целиком, продолжать запуск с ней нельзя.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context