---
title: "Sub(): разбираем конфиг на поддеревья"
description: "viper Sub возвращает поддерево конфига отдельным реестром: изоляция модулей, Sub + Unmarshal, вложенные деревья и nil для несуществующей секции. Примеры."
course: "Viper: конфигурация приложений на Go"
url: "https://golangmaster.ru/courses/viper/lessons/14-sub-derevya/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 16
---

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

*Курс «Viper: конфигурация приложений на Go», урок 14 из 25. ~16 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/viper/lessons/14-sub-derevya/*

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

Два модуля в одном сервисе, и оба читают 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 вернул nil — паника на первом чтении.** Секции нет в конфиге — Sub возвращает nil, и первый же GetString на срезе падает с nil pointer dereference. Обязательный рефлекс: if db == nil до любого чтения. Вторая грань той же ловушки: если под ключом лежит скаляр вместо секции, nil вы не получите, но и валидного поддерева тоже — читаться будут нули или вовсе чужие значения. Под ключом среза живут только секции.

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

Поддерево дружит с [Unmarshal](/courses/viper/lessons/10-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 — глубина не мешает, каждый срез читает свой уровень короткими именами. Про вложенные ключи в общем реестре был [урок одиннадцатый](/courses/viper/lessons/11-vlozhennye-klyuchi/) — Sub добавляет к ним изоляцию. Когда поддеревья лучше плоских ключей: как только настроек у модуля больше трёх-четырёх и внутри появляются свои группы — налоговый модуль с секциями rates и filings, модуль платежей с лимитами и шлюзами. Плоский реестр превращает это в имена вида tax.rates.vat_default, срезы дают каждому модулю его мир. Есть и цена: глубина в конфиге должна отражать структуру модулей, а не удобство автора файла — дерево, повторяющее оргструктуру компании, читать не сможет никто. Два уровня почти всегда достаточны, три — сигнал, что секции пора делить.

> **Срез — снимок на момент вызова.** Поддерево строится из значений родителя на момент вызова Sub. WatchConfig перечитал файл, реестр обновился — а срезы, розданные модулям на старте, остались со старыми значениями. Если приложение живёт на горячей перезагрузке из урока двенадцатого, пересоздавайте срезы в хендлере OnConfigChange и заново раздавайте их модулям, иначе дамп покажет свежие значения, а база будет ходить в старый хост.

### 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") и что случится дальше?

- Пустую *viper.Viper: чтения вернут нули без паники
- ✅ nil-указатель: первое же чтение по нему упадёт с паникой
- Ошибку error, как ReadInConfig на отсутствующем файле
- Поддерево со всеми корневыми ключами

Контракт Sub: нет секции — nil. Отсюда правило урока: проверка if db == nil стоит до первого GetString, иначе nil pointer dereference в рантайме. Пустой экземпляр вы бы получили только при существующей, но пустой секции.

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

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

- ✅ Sub возвращает *viper.Viper, где ключи секции стали корневыми; Get — карту map[string]any
- Ничем: оба возвращают карту
- Sub читает только из файла, Get — из любых источников
- Get возвращает структуру, а Sub — карту

Get отдаёт значение секции как any — карту, которую придётся обходить руками и приводить типы. Sub отдаёт полноценный viper с секцией в роли корня: доступен весь типизированный API, а карта, приведённая вручную, для этого не нужна.

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

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

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

**Что выведет программа?**

- ✅ 10: Sub из Sub вернул поддерево второго уровня, size стал корневым ключом
- 0: вложенные Sub не работают, срез бывает только первого уровня
- Панику: Sub нельзя вызывать на срезе
- Строку "10": значение приехало из yaml как текст

Срез — это полноценный viper, поэтому Sub на нём работает: pool — поддерево database.pool, и GetInt("size") читает его ключ без префиксов. Два уровня вложенности — типичный потолок конфига: глубже начинается чтение с трудом.

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

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

```go
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)
```

Подсказка: Срез даёт Sub, проверяется переменная db, раскладку в структуру делает Unmarshal.

Правильные ответы в пропусках: `Sub`, `db`, `Unmarshal`

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

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

Правильный порядок строк:

1. `db := viper.Sub("database")`
2. `if db == nil { log.Fatal("секции database нет в конфиге") }`
3. `host := db.GetString("host")`
4. `port := db.GetInt("port")`
5. `store := NewStore(host, port)`
6. `log.Println("store готов:", store)`

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

- **Sub** — viper.Sub("database") возвращает *viper.Viper с реестром только из секции: ключи поддерева стали корневыми. Полный API viper на срезе.
- **nil-ответ** — Секции нет — Sub вернёт nil, и первое чтение упадёт с паникой. Проверка if sub == nil — обязательная часть вызова, а не перестраховка.
- **Sub + Unmarshal** — db.Unmarshal(&cfg) раскладывает секцию в структуру с mapstructure-тегами: срез отрезает лишнее, Unmarshal типизирует. Одноразовая альтернатива — UnmarshalKey.
- **Sub из Sub** — Вложенные деревья: db.Sub("pool") отрезает уровень ниже, ключи снова корневые. Глубина конфига повторяет структуру модулей.
- **Get против Sub** — Get("database") — карта map[string]any с ручным обходом; Sub — отдельный реестр с типизированными методами и изоляцией от соседних ключей.
- **Срез и перезагрузка** — Срез — снимок на момент вызова: после перечитывания родителя он остаётся старым. В OnConfigChange поддеревья пересоздают и раздают заново.

### Что дальше

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

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

- 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 через срез базы должно вернуть пустое значение.

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

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

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

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

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

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

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