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

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

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

Урок 10 из 25 ~18 мин +20 XP

Unmarshal: конфиг целиком в структуру

Viper Unmarshal выгружает конфиг целиком в структуру: вместо десятка GetString и GetInt по коду — одно объявление типов и один вызов. Разберём mapstructure-теги, вложенные структуры, Duration и указатели, UnmarshalKey для поддеревьев и ловушку, из-за которой env-значения перестают доезжать до полей.

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

К середине курса чтение конфига разрослось: GetString для хоста, GetInt для порта, GetDuration для таймаута, GetStringSlice для тегов — по строке на ключ, и каждая где-то в коде. Новый ключ — это уже не «добавить строчку в yaml», а найти место чтения, проверить наличие, не опечататься в строковом имени. Десяток ключей держится на внимательности, двадцать — источник багов, которых компилятор не видит. Выход напрашивался давно: описать конфиг структурой — типизированной, с автодополнением и проверкой полей на этапе компиляции — и выгрузить реестр в неё одним вызовом. Это viper.Unmarshal, и он закрывает чтение конфига целиком. Код с Unmarshal исполняется настоящим Go с импортом viper: как и раньше, примеры проверяйте локально.

viper Unmarshal: один вызов вместо десятка Get#

viper.Unmarshal(&cfg) берёт видимую часть реестра и декодирует её в структуру: один вызов вместо цепочки Get. Под капотом — пакет mapstructure: исторически mitchellh/mapstructure, сейчас viper тянет форк go-viper/mapstructure, теги и правила сопоставления не менялись. Контраст с семейством Get из четвёртого урока принципиальный: там каждый ключ читался отдельным вызовом, а несовместимое значение молча становилось нулём; здесь декодер идёт по всей карте значений сразу и возвращает одну ошибку на весь конфиг, если типы не сходятся. Сопоставление полей и ключей работает по умолчанию: поле Port находит ключ port, поле Host — ключ host, регистр не важен. Структура при этом обычная: методы, проверки, компилятор на страже — всё, что даёт язык, вместо строковых имён в рантайме.

Первый Unmarshal

GO
type Config struct {
        Host    string
        Port    int
        Debug   bool
        Retries int
}

var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
        log.Fatalf("конфиг: %v", err)
}

fmt.Println(cfg.Host, cfg.Port, cfg.Debug, cfg.Retries)

Ни одного тега: ключи host, port, debug, retries совпали с полями по имени без учёта регистра. В структуру передаётся указатель — декодер пишет по адресу. Ошибка проверяется и роняет программу на старте: с этого момента несовместимый тип в конфиге перестаёт быть тихим нулём и становится читаемой ошибкой с именем поля. Один нюанс: Unmarshal читает то, что реестр знает, — про env-ловушку ниже отдельный раздел.

mapstructure-теги: имена, регистр и вложенность#

Сопоставление по умолчанию — без учёта регистра по имени поля: ServerPort нашёл бы ключ serverport. Граница ровно на подчёркиваниях и дефисах: ключ server_port в поле ServerPort без тега не попадёт — подчёркивание часть имени, а не деталь регистра. Для таких случаев ставится тег mapstructure: поле объявляется как Port int с mapstructure:"server_port", и декодер соединяет их. Вложенность раскладывается без тегов: ключи database.host и database.port уезжают в поля Host и Port вложенной структуры Database, если у Config есть поле Database этого типа. Тег нужен, когда имя ключа не совпадает с именем поля или конфликтует с ключевым словом Go. Вложенные ключи как тема — доступ по точкам, деревья, GetStringMap — разобраны в отдельном уроке; здесь достаточно знать, что Unmarshal понимает вложенность из коробки.

Типы декодируются с оговорками, которые стоит держать в голове. Длительности: поле Timeout time.Duration примет строку «30s» — viper подключает хук StringToTimeDuration, и декодер парсит время так же, как GetDuration; целое число в таком поле станет наносекундами, врезка из четвёртого урока ещё в силе. Слабая типизация: viper включает WeaklyTypedInput, поэтому строка «8080» уедет в int-поле, число 1 — в bool, пустая строка станет нулём. Указатели — отдельный подарок: поле Retries *int останется nil, когда ключа нет ни в одном слое, и покажет 0, когда ключ есть с нулевым значением. Различие «не задано» против «задано нулём», которого не было у Get, теперь стоит одной звёздочки в объявлении поля — и одной проверки на nil перед разыменованием.

Вложенные структуры, Duration и UnmarshalKey

GO
type Database struct {
        Host string
        Port int
}

type Worker struct {
        Timeout time.Duration `mapstructure:"timeout"`
        Retries *int
}

type Config struct {
        Database Database
        Worker   Worker
}

var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
        log.Fatal(err)
}

var w Worker
if err := viper.UnmarshalKey("worker", &w); err != nil {
        log.Fatal(err)
}

fmt.Println(cfg.Database.Host, cfg.Worker.Timeout, *w.Retries)

Вложенность без тегов: database.host уехал в cfg.Database.Host по совпадению имён. Тег на Timeout — для примера: несовпадающее имя соединяется декларацией, а не переименованием ключей. Retries *int разыменовывается только после проверки на nil — в живом коде сначала if w.Retries != nil. UnmarshalKey прочитал поддерево worker целиком: модуль получает свой срез конфига, не зная остального реестра, — к этому приёму мы вернёмся в уроке про Sub и изоляцию модулей. Запустите у себя локальным Go: конфиг из двух секций — и сравните объём кода с вариантом на Get.

Почему env-значения не доезжают до структуры#

Механика в две строки. Unmarshal не обходит окружение — он берёт список известных ключей (AllKeys), собирает по нему карту значений и декодирует. Список AllKeys собирает ключи из файла, дефолтов, Set, BindEnv и привязанных флагов; значения AutomaticEnv туда не попадают — урок про окружение показывал это на AllSettings. Но есть вторая половина, которая спасает: значения резолвятся через Get, а Get проверяет окружение. Итог: если ключ объявлен — хоть дефолтом, хоть файлом, — env-значение доедет до структуры; если ключ нигде не объявлен, декодер о нём не узнает никогда. Поэтому правило при переходе на Unmarshal: SetDefault на каждое поле — дефолт объявляет ключ и заодно документирует схему, а конфиг в структуру ложится целиком, включая окружение. Альтернатива — BindEnv на каждый ключ: ключ попадает в AllKeys явно, но писать его на каждое поле утомительнее.

Дефолты объявляют ключи — env доезжает

GO
type Server struct {
        Port int
}

type Config struct {
        Server Server
}

viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

// дефолт объявляет ключ: без этой строки env в структуру не доедет
viper.SetDefault("server.port", 8080)

// запуск: SERVER_PORT=9090 ./app
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
        log.Fatal(err)
}

fmt.Println(cfg.Server.Port) // 9090

Ключ server.port объявлен дефолтом — AllKeys его знает, значение резолвится через Get и находит переменную: 9090. Уберите SetDefault — в AllKeys ключа не будет, декодер его пропустит, и в структуре останется ноль. Расхождение между Get и Unmarshal — верный маркер необъявленного ключа: Get вернёт 9090, структура — ноль.

Unmarshal возвращает ошибку — и это заметная смена характера после Get-семейства. cast молча превращал мусор в нули: GetInt на строке «8080порт» давал 0 без единой строки в логе. Декодер mapstructure на несовместимых типах ошибается честно: строка «abc» в int-поле даёт ошибку вида cannot parse 'Server.Port' as int — с путём до поля и первопричиной от strconv; слайс в скалярном поле — ошибка несовместимости. Слабая типизация прощает форму («8080» в int), но не содержание. Ошибку проверяйте и роняйте старт: это бесплатная валидация типов конфига, которой у Get не было вовсе. Рядом живёт UnmarshalExact — вариант декодера, который падает на незнакомых ключах: годится как проверка схемы, пока в yaml не заведутся «лишние» ключи от старых версий; тогда жёсткость станет обузой, и обычный Unmarshal спокойнее.

Get против Unmarshal: когда что#

КритерийСемейство GetUnmarshal
Объём кодастрока на каждый ключодин вызов на весь конфиг
Несовместимый типcast молча даёт нольошибка декодирования с именем поля
Ключа нетнулевое значение типаполе нулевое; поле-указатель — nil
env через AutomaticEnvвидна при каждом Getдоедет, только если ключ объявлен SetDefault или BindEnv
Вложенные ключичерез точку: database.hostвложенные структуры
ПоддеревоGetStringMap и вручнуюUnmarshalKey в отдельную структуру
Опечатка в чтениистроковое имя — ошибка в рантаймеполе структуры — ошибка компиляции

Гигиена Unmarshal#

  • Дефолт на каждое поле: ключ объявлен — env доезжает, IsSet отвечает честно, схема задокументирована одним местом.
  • Ошибка Unmarshal — сигнал к падению на старте: несовместимые типы ловите до поднятия серверов, а не в дежурство.
  • Длительности в конфиге держите строками «30s»: числовое значение в поле time.Duration станет наносекундами.
  • Поля-указатели — для различения «не задано» и «ноль»: nil перед разыменованием проверяйте всегда.
  • Секретные поля не печатьте вместе со структурой: маска в String() или отдельный вывод только нечувствительных полей.
  • Поддеревья модулей читайте UnmarshalKey: меньше связей с реестром, а изоляция модулей уже готова.

Проверка: ключ server_port

Ключ в конфиге — server_port, поле структуры — ServerPort без тега. Что произойдёт при Unmarshal?

Проверка: env и пустой реестр

AutomaticEnv включена, переменная SERVER_PORT=9090 задана. В коде — только viper.Unmarshal в структуру с полем Port; ни файла, ни SetDefault, ни BindEnv. Что окажется в поле?

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

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

// config.yaml: worker: {retries: "abc"}
type Worker struct {
        Retries int
}

var w Worker
err := viper.UnmarshalKey("worker", &w)
fmt.Println(w.Retries, err != nil)

Соберите чтение в структуру

Допишите выгрузку всего конфига, чтение поддерева, объявление ключа дефолтом и доступ к вложенному полю.

var cfg Config if err := viper.(&cfg); err != nil { log.Fatal(err) } var w Worker if err := viper.("worker", &w); err != nil { log.Fatal(err) } viper.("server.port", 8080) fmt.Println(cfg.Server.)

Соберите секцию, где env доезжает до структуры

Расставьте строки: replacer, окружение, объявление ключа, декодирование в структуру и вывод.

Строки

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

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

Карточки: Unmarshal и mapstructure

Термин

viper.Unmarshal

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

Выгружает видимую часть реестра в структуру одним вызовом; под капотом mapstructure. Несовместимые типы дают ошибку, а не молчаливый ноль.

1 / 6

Что дальше#

Конфиг теперь типизированная структура: одно объявление вместо десятка строковых вызовов, ошибки типов — на старте, опечатки в именах — компилятором. Дочитали до точки, где viper почти закрыт как «чтение по ключам»: дальше — вложенные ключи во всей полноте: деревья, точки и mapstructure рядом, затем WatchConfig и живая перезагрузка без перезапуска. Прежде чем двигаться, перенесите чтение своего проекта на структуру: одна сборка Unmarshal вместо разбросанных Get обычно находит пару ключей, которые читались не оттуда. Домой, к маршруту по урокам и темам, ведёт страница курса viper.

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

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

Вывод

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

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

  • viper.Unmarshal(&cfg) выгружает конфиг в структуру одним вызовом вместо десятка Get: поля типизированы, опечатки ловит компилятор.
  • Имена полей сопоставляются ключам без учёта регистра; для server_port и других «негладких» имён ставится тег mapstructure.
  • Вложенные ключи раскладываются по вложенным структурам: database.host уезжает в Config.Database.Host; UnmarshalKey читает поддеревья.
  • Строки «30s» декодируются в time.Duration хуком по умолчанию; поле-указатель различает «ключа нет» и «ноль».
  • env-значения через AutomaticEnv не доезжают до Unmarshal, пока ключ не объявлен: SetDefault на каждое поле или BindEnv.
  • Несовместимые типы дают ошибку декодирования с именем поля — бесплатная валидация на старте вместо молчаливых нулей cast.
  • Структуру конфига не печатайте целиком: все поля, включая секреты, уходят в лог одной строкой.

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

Переведите чтение проекта из прошлого урока на структуру: секции Server и Worker, Timeout time.Duration, один Unmarshal и один UnmarshalKey. На каждое поле — SetDefault, затем запуск с SERVER_PORT=9090: env должен доехать до поля. Подложите в yaml retries: "abc" и прочитайте ошибку декодирования целиком.

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

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

Unmarshal забирает секреты из env?

Забирает — если ключ объявлен дефолтом или BindEnv: значение резолвится тем же Get с проверкой окружения. Опасность в другом: печать структуры целиком показывает все поля открыто. Маскируйте секретные поля отдельным типом со String() и не отправляйте %+v от cfg в общий журнал.

Что даёт UnmarshalExact?

Падение на незнакомых ключах: опечатка в yaml перестаёт быть тихой, схема проверяется декодером. Цена — жёсткость: «лишние» ключи от старых версий конфига не дадут стартовать. Для устоявшегося набора ключей полезно, для строящейся схемы спокойнее обычный Unmarshal.

Поле с тегом не заполнилось — где искать?

Три кандидата. Путь: тег ставится на поле секции, а не на верхнем уровне — для database.host тег нужен на Host внутри Database. Имя: подчёркивания значимы, тег должен совпадать посимвольно. Тип: ошибка декодера выше по стеку — проверяйте err, а не только поле.

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

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

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

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

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

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

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