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

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

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

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

Sub(): разбираем конфиг на поддеревья

viper Sub отрезает от общего реестра поддерево: секция database становится отдельной viper, где host — это хост базы, а не веб-сервера. На плоских ключах модули конфликтуют за имена, на поддеревьях живут изолированно. Разберём механику Sub, связку с Unmarshal, вложенные деревья и nil для несуществующей секции.

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

Два модуля в одном сервисе, и оба читают GetString("host") из общего реестра: первому нужен хост веб-сервера, второму — хост базы. Пока ключи уникальны, плоский реестр работает; как только названия пересекаются, начинаются тихие подмены значений, которые ловятся только в проде. viper Sub решает это структурно: конфиг разрезается на поддеревья, и каждый модуль получает свой срез — секцию database целиком, внутри которой ключи снова называются коротко. Разберём механику Sub: что возвращается, как связать поддерево с Unmarshal, зачем нужны вложенные Sub и чем срез отличается от Get("database") с картой. История о хосте веб-сервера в настройках базы покажет, где плоские ключи ломаются первыми. Примеры читают yaml-файлы, поэтому для запуска нужен локальный Go: песочница сайта конфиги не читает.

viper Sub(): поддерево как отдельный реестр#

viper.Sub("database") возвращает *viper.Viper — новый экземпляр, чей реестр содержит только содержимое секции database. Ключи внутри среза называются как в исходной секции: sub.GetString("host") читает database.host, но пишется без префикса. Возвращаемое значение — не копия данных в новом формате, а полноценный viper: доступны GetString, GetInt, GetDuration, IsSet, Unmarshal, Set — весь привычный API. Отсюда и название паттерна: модуль получает срез и дальше живёт в нём, не зная, как его секция называлась в общем конфиге. Ключевая деталь контракта: секции нет — Sub вернёт nil, а не пустой экземпляр, и первое же чтение на nil-указателе падает с паникой. Проверка на nil — часть вызова, а не перестраховка; ниже — врезка с разбором этой ловушки и соседней: скаляром вместо секции.

Первый срез

GO
// config.yaml:
// database: {host: "db.local", port: 5432}
// server:   {host: "web.local", port: 8080}

db := viper.Sub("database")
if db == nil {
        log.Fatal("секции database нет в конфиге")
}

fmt.Println(db.GetString("host"))           // db.local
fmt.Println(db.GetInt("port"))              // 5432
fmt.Println(viper.GetString("server.host")) // web.local

Срез читает ключи без префикса database.: host внутри db — это database.host из файла, и путаницы с server.host больше нет. Корневой реестр продолжает видеть всё целиком — срез не вырезает данные, а открывает отдельное окно на секцию.

Изоляция: модуль БД не видит соседних ключей#

Смысл поддеревьев — не краткая запись, а границы. Передайте NewStore срез database, и модуль физически не сможет прочитать server.port, worker.timeout или ключи соседей: их в его реестре просто нет. Это изоляция в действии — то же правило, что у пакетов и интерфейсов: сузили доступ, сузили поверхность ошибок. Модуль не зависит от имени своей секции: переименование database в postgres меняет одну строку на границе, а внутри модуля не меняется ничего — срезы строят снаружи и передают внутрь. И наоборот: модуль не может случайно записать Set-ключ, который попадёт в чужую секцию, — его Set работает только в его срезе. На проектах с несколькими командами это снимает целый класс споров о схеме конфига: каждая команда владеет своей секцией и её срезом, а общий корень остаётся только местом сборки. Дополнительный бонус подхода — тесты: модульный тест может собрать Sub-реестр из строки и проверить поведение модуля без общего конфига приложения.

Конструктор, принимающий срез

GO
func NewStore(cfg *viper.Viper) (*Store, error) {
        if cfg == nil {
                return nil, errors.New("срез конфига БД не передан")
        }
        host := cfg.GetString("host")
        port := cfg.GetInt("port")
        timeout := cfg.GetDuration("timeout")
        if host == "" || port <= 0 {
                return nil, errors.New("host и port обязательны")
        }
        return &Store{dsn: fmt.Sprintf("postgres://%s:%d?timeout=%s", host, port, timeout)}, nil
}

// сборка на границе main:
store, err := NewStore(viper.Sub("database"))

Конструктор принимает срез и читает ключи без префиксов: host здесь — database.host из файла. Проверка на nil и границы значений живут на границе модуля: пустой host ловится до первого соединения. Заметьте: NewStore не знает слова database — переименование секции его не заденет.

Спор: viper в конструкторе или структура на границе#

Передавать viper.Viper в конструкторы или маппить в структуру — самый спорный вопрос темы. Сторона среза в конструкторе выигрывает гибкостью: модуль читает всё, что появилось в его секции, без правки структур, а лениво добавленные ключи подхватываются без обновления контракта. Я в продуктовом коде выбираю обратное: на границе пакета config срез раскладывается в структуру через Unmarshal, и дальше модуль получает обычные данные — структуру с полями, проверками и тестами, в которой viper нет вообще. Моё правило жёстче, чем советуют примеры: viper не должен покидать пакет config, иначе библиотека расползается по коду, и заменить её однажды будет некому. Граница применимости честная: утилита на вечер или проект с одним main — прямой viper.Viper в конструкторе абсолютно нормален, цена замены копеечная. Как оформить такой пакет целиком — тема урока восемнадцатого, а механику маппинга разберём ниже.

Sub и Unmarshal: типизированный срез#

Поддерево дружит с Unmarshal так же, как корневой реестр: db.Unmarshal(&cfg) раскладывает секцию в структуру с mapstructure-тегами. Это связка, которой я пользуюсь чаще всего: срез отрезает лишнее, Unmarshal типизирует — и модуль получает обычную структуру с полями и проверками. Бонус — изоляция работает и на этапе маппинга: чужие ключи в структуру не попадут, даже если секция разрослась. Тот же результат даёт UnmarshalKey("database", &cfg) на корневом viper — он строит временный срез под капотом. Разница в жанре: UnmarshalKey — одноразовое чтение, а срез, сохранённый в переменной, живёт дальше — его можно передать конструктору, дополнить Set-правками модуля, обернуть своей обёрткой. Для горячей перезагрузки у обеих схем одна проблема — устаревший слепок значений, о ней врезка ниже.

Срез раскладывается в структуру

GO
type DBConfig struct {
        Host    string        `mapstructure:"host"`
        Port    int           `mapstructure:"port"`
        Timeout time.Duration `mapstructure:"timeout"`
}

var dbCfg DBConfig
db := viper.Sub("database")
if db == nil {
        log.Fatal("секции database нет в конфиге")
}
if err := db.Unmarshal(&dbCfg); err != nil {
        log.Fatal(err)
}
fmt.Println(dbCfg.Host, dbCfg.Port, dbCfg.Timeout)

Теги mapstructure те же, что и у корневого Unmarshal: типы приводит mapstructure, поле Timeout понимает строку 30s. Модуль дальше работает со структурой, а не с viper: конструктор с DBConfig тестируется картой значений без файлов и окружения.

Sub из Sub: вложенные деревья#

Секция внутри секции — не проблема: db.Sub("pool") вернёт поддерево второго уровня, где ключи pool снова станут корневыми. Так разбираются многоуровневые настройки: database.pool.size, payments.limits.day — глубина не мешает, каждый срез читает свой уровень короткими именами. Про вложенные ключи в общем реестре был урок одиннадцатый — Sub добавляет к ним изоляцию. Когда поддеревья лучше плоских ключей: как только настроек у модуля больше трёх-четырёх и внутри появляются свои группы — налоговый модуль с секциями rates и filings, модуль платежей с лимитами и шлюзами. Плоский реестр превращает это в имена вида tax.rates.vat_default, срезы дают каждому модулю его мир. Есть и цена: глубина в конфиге должна отражать структуру модулей, а не удобство автора файла — дерево, повторяющее оргструктуру компании, читать не сможет никто. Два уровня почти всегда достаточны, три — сигнал, что секции пора делить.

Get против Sub на одной секции#

Вопросviper.Get("database")viper.Sub("database")
Возвращаетmap[string]any — значение секции*viper.Viper — отдельный реестр
Чтение вложенных ключейviper.Get("database.host") или обход картыsub.GetString("host") — секция как корень
Секции нетnil-значение без ошибкиnil-указатель — нужна проверка
Что дальше рукамиtype assertion и ручной обходвесь API: Get, IsSet, Unmarshal, Set

Когда поддеревья лучше плоских ключей#

  • У модуля больше трёх-четырёх настроек, и внутри них свои группы: rates, limits, retries.
  • Настройки принадлежат разным командам: секция — граница владения, срез — её API.
  • Названия ключей пересекаются между модулями: host и port есть и у сервера, и у базы.
  • Модуль тестируется изолированно: срез собирается из карты в тесте без файлов и окружения.
  • Конфиг описывает структуру системы: database, payments, tax — дерево повторяет модули.
  • Планируется вынос модуля в отдельный сервис: его секция уже готова стать его конфигом.

Проверка: секция отсутствует

Секции database в конфиге нет. Что вернёт viper.Sub("database") и что случится дальше?

Проверка: Sub против Get

Чем viper.Sub("database") принципиально отличается от viper.Get("database")?

Что выведет вложенный срез?

Прочитайте код и выберите, что он выведет:

// config.yaml:
// database:
//   host: "db.local"
//   pool:
//     size: 10

db := viper.Sub("database")
pool := db.Sub("pool")
fmt.Println(pool.GetInt("size"))

Соберите связку Sub плюс Unmarshal

Допишите получение среза, проверку на nil и раскладку секции в структуру.

db := viper.("database") if == nil { log.Fatal("секции database нет в конфиге") } var cfg DBConfig if err := db.(&cfg); err != nil { log.Fatal(err) } fmt.Println(cfg.Host, cfg.Port)

Соберите передачу среза в модуль

Расставьте строки: получить срез, проверить на nil, прочитать два значения и собрать хранилище.

Строки

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

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

Карточки: поддеревья конфига

Термин

Sub

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

viper.Sub("database") возвращает *viper.Viper с реестром только из секции: ключи поддерева стали корневыми. Полный API viper на срезе.

1 / 6

Что дальше#

Срезы нарезаны, и следующий урок — регистрация алиасов: переименовываем ключи схемы, не ломая пользователей. Полный маршрут по темам лежит на странице курса viper, а база под структуры и теги — в уроке про структуры курса go-basics, если поля и mapstructure требуют повторения. Домашний опыт — локальным Go: нарежьте конфиг на срезы database и worker, передайте их конструкторам и добейтесь, чтобы попытка прочитать worker.timeout через срез базы вернула пустое значение — это и есть изоляция, которую можно потрогать руками.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • viper.Sub("database") возвращает *viper.Viper, чей реестр — только секция: ключи поддерева читаются без префикса.
  • Секции нет — Sub возвращает nil, и первое чтение падает с паникой: проверка на nil стоит до любого GetString.
  • Срез — изоляция: модуль не видит соседних ключей и не может записать чужой Set, а переименование секции его не заденет.
  • Связка с Unmarshal: db.Unmarshal(&cfg) типизирует срез в структуру с mapstructure-тегами; одноразовая альтернатива — UnmarshalKey.
  • Sub работает на срезе: db.Sub("pool") отрезает вложенный уровень — глубина конфига повторяет структуру модулей.
  • Get("database") — карта для ручного обхода, Sub — типизированный API: поддеревья лучше плоских ключей, когда модулей много и ключи пересекаются.
  • Правило пакета config: viper не покидает его границы — наружу идут структуры, а не *viper.Viper.

Домашнее задание

Вынесите секции database и worker в отдельные срезы через Sub и передайте их конструкторам NewStore и NewWorker. Добавьте проверку на nil для отсутствующей секции и убедитесь локальным Go, что модуль не видит чужих ключей: чтение worker.timeout через срез базы должно вернуть пустое значение.

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · Sub(): разбираем конфиг на поддеревья)

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

Видит ли срез изменения родителя после горячей перезагрузки?

Нет: поддерево строится из значений родителя на момент вызова Sub. WatchConfig перечитает файл, реестр обновится, а уже розданные срезы останутся со старыми значениями. Рабочий приём — пересоздавать поддеревья в хендлере OnConfigChange и заново раздавать их модулям.

Передавать ли срез *viper.Viper в конструкторы модулей?

Для утилит и проектов с одним main — нормально, цена замены копеечная. В продуктовом коде я раскладываю срез в структуру на границе пакета config и наружу выпускаю только данные: viper не должен расползаться по коду, иначе заменить библиотеку однажды будет некому.

Чем Sub лучше Get("database") с картой?

Карта требует ручного обхода и приведения типов, а тип в значениях зависит от формата файла. Срез даёт весь типизированный API — GetString, GetInt, Unmarshal — плюс изоляцию: соседние ключи в реестре среза отсутствуют, и модуль не может случайно прочитать или записать чужое.

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

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

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

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

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

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

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