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

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

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

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

JSON, TOML и другие форматы конфигов

Переписывать код чтения под каждый формат конфигов не станет никто: viper json toml и остальные форматы читает одним API, парсер выбирается по расширению файла, а программа не меняется. Ситуация знакомая: соседний отдел живёт в TOML, генератор деплоя выплёвывает JSON, легаси-сервис тянет ini. Разберём список форматов, порядок поиска файла и чтение конфига из строки.

Содержание урока · 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 об этом даже не узнает. Отдельные парсеры подключать не нужно — всё уже внутри библиотеки. Переключение формата — это замена файла и, в отдельных случаях, одной строки настройки, но не правка кода чтения.

Поиск файла без расширения

GO
viper.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, который поиск отключает вовсе. Для парсера форматы равноправны, но не для поиска: разница между «поддерживается» и «будет найдено первым» здесь принципиальная, и о ней следующая врезка.

Тот же код читает TOML

GO
package 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 из строки

GO
payload := []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=valueenv-файлы деплоя, java-наследиетипов и вложенности нет

ReadInConfig возвращает разные ошибки для разных провалов, и различать их стоит с первого дня. Когда ни один путь не дал файла, прилетает viper.ConfigFileNotFoundError — тип, который проверяют через errors.As. Это штатная ситуация, а не авария: программа может продолжить жизнь на значениях по умолчанию, и как готовить её к конфигу-которого-нет, разобрано в уроке про SetDefault. Совсем другое дело — ошибка парсинга: файл нашёлся, но синтаксис битый — незакрытая кавычка, таб вместо пробелов в yaml. Конфиг при этом не загружается целиком: viper не читает «половину файла», и это правильное поведение — запуск с полузагруженными настройками хуже падения на старте. Сообщение называет файл и причину, но не строку — blame по недавнему коммиту быстрее гаданий. Третьей категории, «файл есть, но ключа в нём нет», для ReadInConfig не существует: отсутствующий ключ — не ошибка чтения, а вопрос к тому, кто читает значения, и об этом следующий урок.

Признаки, что формат выбран плохо#

  • Диффы конфига в ревью нечитабельны: одна правка порождает десять строк шума — формат не для человека.
  • Каждый новый ключ приходится объяснять коллегам вслух: комментариев нет, а это json, который правят руками.
  • Три уровня вложенности, и половина багов — забытый отступ: глубокие деревья в ini и properties не живут.
  • Одна и та же настройка описана в двух файлах разных форматов, и никто не помнит, какой победит, — см. порядок SupportedExts.
  • Конфиг генерируется скриптом, а правится руками, и каждый прогон сносит комментарии: машинный и человеческий форматы смешаны в одном файле.

Проверка: какой файл победит

В папке ./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"))

Соберите поиск конфига

Допишите: имя без расширения, путь поиска, подсказку формата для файла без расширения и сам вызов чтения.

viper.("config") viper.("./configs") viper.("yaml") // файл configs/config без расширения err := viper.()

Соберите загрузку конфига по строкам

Расставьте строки: имя конфига, два пути поиска, подсказка формата, чтение с обработкой ошибки и вывод значения.

Строки

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

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

Карточки: форматы и поиск

Термин

SupportedExts

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

Фиксированный порядок расширений поиска в viper: json, toml, yaml, yml, properties, props, prop, hcl, tfvars, dotenv, env, ini. Первый существующий файл выигрывает — json в одной папке сильнее yaml.

1 / 6

Что дальше#

Форматы разобраны: файл найден, реестр заполнен — осталось вынуть из него значения, и здесь начинается самое интересное. Семейство 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 · JSON, TOML и другие форматы конфигов)

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

Какие форматы 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 означает, что поиск прошёл впустую, и программа может стартовать на дефолтах. Любая другая ошибка — парсинг: файл найден, синтаксис битый, конфиг не загружен целиком, продолжать запуск с ней нельзя.

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

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

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

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

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

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

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