Конфиг из прошлого урока загрузился, дерево ключей лежит в реестре — и начинается часть, которая выглядит скучной, а ломает больше всего программ: чтение значений. Наивные вопросы здесь самые дорогие. Что вернёт viper.Get, если ключа нет? Почему GetInt спокойно читает порт из строки «8080»? Почему код с type assertion паникует на json-конфиге, который вчера работал на yaml? Ответы сводятся к одному устройству: каждый типизированный метод — это общий Get плюс пакет cast, который приводит значения со своими молчаливыми соглашениями. Разберём методы по одному, поведение при отсутствующем ключе, IsSet и AllKeys. И честно про практику: эти примеры читают реальные файлы, поэтому запускаются локальным Go — браузерная песочница файлового ввода-вывода не умеет.
viper GetString и семейство: общий принцип#
У viper нет отдельных методов под каждый источник и формат — значения читаются из единого реестра, и неважно, приехали они из yaml, toml или из строки через ReadConfig. Семейство такое: GetString, GetInt, GetBool, GetFloat64, GetDuration, GetTime, GetStringSlice, GetStringMap и главный Get, возвращающий any. Каждый типизированный метод — это Get плюс cast: GetString вызывает cast.ToString, GetInt — cast.ToInt, и пакет старается привести значение к типу, а не ругаться на несовпадение. Ключи вложенных секций берутся через точку: GetString("server.host") — это host внутри server. Отдельного GetMap нет — для карт служит GetStringMap, для списков GetStringSlice, и оба на отсутствующем ключе возвращают пустые значения, а не ошибки. Правило выбора простое: знаете тип — берите типизированный метод; не знаете — Get, но разбирайтесь с результатом сами, чем он и опасен.
Семейство Get в одной программе
GO// configs/config.yaml:
// server: {host: "0.0.0.0", port: 8080, debug: false}
// worker: {timeout: 30s, retries: "3", tags: [go, api]}
fmt.Println(viper.GetString("server.host")) // 0.0.0.0
fmt.Println(viper.GetInt("server.port")) // 8080
fmt.Println(viper.GetBool("server.debug")) // false
fmt.Println(viper.GetDuration("worker.timeout")) // 30s
fmt.Println(viper.GetInt("worker.retries")) // 3
fmt.Println(viper.GetStringSlice("worker.tags")) // [go api]Обратите внимание на worker.retries: в yaml это строка "3" в кавычках, а GetInt всё равно вернёт число — cast приводит значения, не спрашивая. Duration понимает синтаксис time.ParseDuration: 30s, 15m, 1h30m. Комментарии справа показывают вывод, а не синтаксис: в живом коде эти значения уезжают в конструктор сервера и воркера. Разнобой типов в самом конфиге — повод для ревью: числа лучше писать числами.
GetInt из строки «8080»: как работает cast#
Приведение типов в viper — это пакет github.com/spf13/cast из-под крыла spf13, тот же, что использует Hugo. Правила стоит знать наизусть. Строка в число: GetInt("8080") вернёт 8080 — внутри strconv.Atoi. Строка в булево: GetBool прогоняет значение через strconv.ParseBool, который принимает true, True, TRUE, 1, t, T — а всё остальное молча даёт false. Длительность: GetDuration("15m") парсится как время, зато целое число станет наносекундами — об этом отдельная врезка ниже. Обратное направление тоже работает: GetString(8080) даст "8080", что удобно в логах. Главный принцип cast: ошибка конвертации наружу не возвращается — вместо неё вы получаете нулевое значение типа. GetInt("8080порт") — это 0 без предупреждения, GetBool("да") — false. Поэтому конфиг с сомнительными значениями проверяют валидацией на старте, которой в курсе отведён отдельный урок.
Целое число в GetDuration трактуется как наносекунды: timeout: 300 из конфига — это 0.0000003 секунды, а не 300 секунд и не 300 миллисекунд. Симптом узнаваемый: таймауты не срабатывают, потому что истекают мгновенно. Лечение — хранить длительность строкой: timeout: 300s. Либо храните миллисекунды и умножайте сами: GetInt("timeout_ms") * time.Millisecond.
Нет ключа — нет ошибки: нулевые значения#
Ни один метод семейства не возвращает error: отсутствующий ключ даёт нулевое значение типа. Get — nil, GetString — пустая строка, GetInt — 0, GetBool — false, GetDuration — нулевая длительность, GetTime — время 0001-01-01. Дизайн осознанный: чтение конфига не может уронить программу посреди логики. Цена — знаменитая ловушка неразличимости: GetInt("server.port") возвращает 0 и когда порт в конфиге равен нулю, и когда ключа нет вообще, и когда значение не конвертируется. Дальше ноль едет по программе: http.ListenAndServe(":0") честно слушает случайный порт, который вы потом не найдёте, а таймаут 0 у многих клиентов означает «без таймаута», и запросы висят вечно. Выход — viper.IsSet(key): она отвечает на вопрос «а существует ли ключ», не путаясь в значениях. Пара IsSet плюс чтение покрывает обе ситуации, и для критичных настроек должна стать рефлексом.
IsSet: отличаем ноль от отсутствия
GOif !viper.IsSet("server.port") {
log.Fatal("нет ключа server.port: проверьте конфиг")
}
port := viper.GetInt("server.port")
if port <= 0 {
log.Fatal("server.port должен быть положительным числом")
}IsSet отвечает true, когда ключ существует в любом источнике — файле, дефолтах, явных значениях. Первая проверка отделяет «ключа нет» от «значение 0», вторая отсекает бессмысленные значения. Дефолт на такой случай задаётся SetDefault — об этом следующий урок: после него IsSet станет всегда true, и останется одна проверка границ.
Метод, тип и поведение без ключа#
| Метод | Возвращает | Нет ключа | Строка из конфига |
|---|---|---|---|
| Get | any | nil | приведений не делает — тип как в файле |
| GetString | string | пустая строка | число превратится в текст |
| GetInt | int | 0 | "8080" станет числом, мусор — нулём |
| GetBool | bool | false | ParseBool: true, True, 1, t; остальное — false |
| GetFloat64 | float64 | 0 | "3.14" станет числом |
| GetDuration | time.Duration | 0 | "15m" распарсится; целое число — наносекунды |
| GetTime | time.Time | нулевое время 0001-01-01 | несколько форматов, RFC3339 в том числе |
| GetStringSlice | []string | пустой срез (len 0) | yaml-список или строка, разбитая по пробелам |
| GetStringMap | map[string]any | пустая карта (len 0) | вложенная секция целиком |
Три метода заслуживают отдельного прохода. GetStringSlice читает списки: yaml-массив превращается в []string, а одиночная строка разбивается по пробелам, так что «go api» в кавычках даст тот же срез из двух элементов. GetStringMap возвращает вложенную секцию целиком как map[string]any — удобно, когда структура секции заранее неизвестна; на отсутствующей секции приходит пустая карта. GetTime пробует несколько форматов даты, от RFC3339 до 2006-01-02, и на неудаче возвращает нулевое время 0001-01-01 — проверяйте его методом IsZero. Все три ведут себя как остальное семейство: без ошибок, с пустыми значениями на промахе. Для строгой схемы вместо ручного обхода карт существует Unmarshal — он разложит конфиг в структуры одним вызовом, и это материал десятого урока.
Get — универсальный люк в реестр: он возвращает any без приведений, и это одновременно спасение и мина. Спасение — когда тип значения заранее неизвестен: обход AllKeys, отладочная печать, динамические секции. Мина — type assertion. На yaml-конфиге порт приходит числом int, и Get("server.port").(int) работает; смените файл на json — и все числа после json-парсинга становятся float64, отчего тот же код падает с interface conversion panic. Два формата, одна строка кода, разные типы в рантайме — верный признак того, что приведение должно делать семейство Get, а не ваш код: GetInt съест и int, и float64, и строку. Моё правило, спорное ровно наполовину: в продуктовом коде Get почти не нужен — типизированные методы и Unmarshal из урока десятого покрывают всё, а any держать в руках стоит только для отладки. Граница применимости честная: если схема конфига действительно динамическая — генераторы, плагины, произвольные деревья — Get правильный выбор, потому что имена ключей и типы не знает заранее никто.
AllKeys: обход реестра
GOfor _, k := range viper.AllKeys() {
fmt.Println(k, "=", viper.Get(k))
}
// вывод по config.yaml со списком ports: [8080, 8081]:
// server.host = 0.0.0.0
// server.ports.0 = 8080
// server.ports.1 = 8081
// worker.timeout = 30sПорядок строк вывода в реальном запуске не гарантирован: AllKeys собирается из карт, а карты в Go перебираются в случайном порядке — для стабильного дампа отсортируйте срез. Индексы массивов — не синтаксис файла, а представление реестра: GetInt("server.ports.1") вернёт 8081. Обратная операция — AllSettings(): всё дерево одной картой map[string]any для дампа на старте. Нюанс: ключи из окружения через AutomaticEnv в AllKeys не появятся — список переменных процесса перечислить нельзя, это ограничение viper, о котором честно сказано в уроке про окружение.
GetInt("8080 ") с пробелом на конце, GetBool("да"), GetFloat64("три") — все вернут нули без единой строчки в логе: cast проглатывает ошибку конвертации. Всплывает это обычно в проде, уже после ревью конфига. Защита одна: валидация критичных ключей IsSet-ом и проверками границ на старте программы, пока не подняты серверы и воркеры.
Гигиена чтения значений#
- Знаете тип — берите типизированный метод: GetString вместо Get, GetInt вместо строковых игр с приведением на месте.
- Критичные ключи проверяйте IsSet-ом и границами до старта серверов: порт больше нуля, таймаут не нулевой.
- Длительности храните строками с суффиксом: 30s, 15m — целые числа GetDuration считает наносекундами.
- Type assertion на значениях Get не пишите: приведение — работа GetInt и компании, а не вашего кода.
- После ReadInConfig один раз напечатайте дамп AllKeys с AllSettings: сразу видно, что реально загрузилось и в каком виде.
- Реестр читается на момент загрузки: правки файла после ReadInConfig сами не подхватятся — для этого существует WatchConfig, отдельная механика из двенадцатого урока.
Проверка: GetInt вернул 0
viper.GetInt("server.port") вернул 0. Какие объяснения возможны?
Проверка: GetBool и строка в кавычках
В yaml написано debug: "True" — значение в кавычках, то есть строка. Что вернёт viper.GetBool("debug")?
Что выведет таймер?
Прочитайте код и выберите, что он выведет:
// configs/config.yaml: timeout: "15m"
d := viper.GetDuration("timeout")
fmt.Println(d)Соберите чтение с проверкой
Допишите типизированное чтение строки и числа, проверку наличия ключа и обход всех ключей конфига.
Соберите чтение конфига по строкам
Расставьте строки: явный файл, чтение с обработкой ошибки, три значения и вывод.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: чтение значений
Что дальше#
Реестр читается, ловушки разминированы. Дальше по курсу — значения по умолчанию, чтобы свежий деплой с пустым конфигом не стартовал на нулях, затем окружение и флаги, которые перекрывают файл по приоритетам. Домой, к маршруту по всем урокам и темам, ведёт страница курса viper. Домашний опыт на реальных файлах: соберите configs/config.yaml с разнотипными ключами и массивом портов, прогоните дамп AllKeys локальным Go — индексы вида server.ports.0 в выводе убедительнее любого объяснения. Заодно проверьте собственный код: где вы различаете «ключа нет» и «ноль», а где эта пара до сих пор сливается в один GetInt.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Семейство Get — это Get плюс cast: GetString, GetInt, GetBool, GetFloat64, GetDuration, GetTime, GetStringSlice, GetStringMap; приведение типов внутри, ошибки наружу не выходят.
- GetInt("8080") читает порт из строки, GetString(8080) превращает число в текст — cast конвертирует в обе стороны.
- Отсутствующий ключ даёт нулевое значение: Get — nil, GetString — пустая строка, GetInt — 0; различить «нет ключа» и «ноль» помогает IsSet.
- GetDuration понимает "15m", но целое число трактует как наносекунды — длительности храните строками.
- Get возвращает any: числа из json приходят float64, из yaml — int, и type assertion на том же коде падает.
- AllKeys возвращает плоские ключи с индексами массивов: server.ports.0; AllSettings — всё дерево одной картой.
- Критичные настройки проверяйте на старте: IsSet плюс границы значений — дешёвая страховка от молчаливых нулей cast.
Домашнее задание
Соберите configs/config.yaml с тремя ключами: строкой в кавычках, числом и длительностью 30s. Прочитайте их GetString, GetInt и GetDuration, напечатайте значения и типы через fmt.Printf с глаголом %T. Затем удалите один ключ из файла и добавьте IsSet-проверку: программа должна сообщать, какого ключа нет, вместо чтения нуля.
Личные заметкиПусто
Вопросы и ответы по уроку
GetInt читает порт из строки «8080» — это безопасно?
Безопасность тут не в типе, а в границах: cast вызывает strconv.Atoi и молча даёт 0 на мусоре. Для конфига это обычно удобство — деплой-инструменты часто отдают всё строками, — но критичные значения проверяйте границами: порт больше нуля, таймаут не нулевой.
Чем GetStringMap отличается от Get?
GetStringMap сразу даёт map[string]any — вложенную секцию целиком, удобно для динамических деревьев. Get вернул бы то же значение как any, но без типизированного объявления. Оба не отличают отсутствующую секцию от пустой: и там, и там длина ноль.
Можно ли узнать, из какого источника пришло значение — файл, окружение или дефолт?
Прямого API нет: реестр viper не хранит происхождение значений. Практический путь — эксперимент по лестнице приоритетов: закомментируйте ключ в файле, задайте переменную окружения, напечатайте AllSettings и сравните. Диагностика таких случаев — тема урока двадцать второго.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context