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

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

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

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

Чтение значений: GetString, GetInt и семейство Get

Конфиг из прошлого урока загружен, и за чтение значений отвечает viper GetString с семейством типизированных Get: cast приводит типы сам — порт из строки «8080», длительность из «15m». Ошибок нет вообще — вместо них нулевые значения, и это лучше знать заранее. Разберём каждый метод, поведение при отсутствующем ключе, IsSet, AllKeys и молчаливые нули.

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

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

Нет ключа — нет ошибки: нулевые значения#

Ни один метод семейства не возвращает 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: отличаем ноль от отсутствия

GO
if !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, и останется одна проверка границ.

Метод, тип и поведение без ключа#

МетодВозвращаетНет ключаСтрока из конфига
Getanynilприведений не делает — тип как в файле
GetStringstringпустая строкачисло превратится в текст
GetIntint0"8080" станет числом, мусор — нулём
GetBoolboolfalseParseBool: true, True, 1, t; остальное — false
GetFloat64float640"3.14" станет числом
GetDurationtime.Duration0"15m" распарсится; целое число — наносекунды
GetTimetime.Timeнулевое время 0001-01-01несколько форматов, RFC3339 в том числе
GetStringSlice[]stringпустой срез (len 0)yaml-список или строка, разбитая по пробелам
GetStringMapmap[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: обход реестра

GO
for _, 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, о котором честно сказано в уроке про окружение.

Гигиена чтения значений#

  • Знаете тип — берите типизированный метод: 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)

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

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

host := viper.("server.host") if viper.("server.port") { port := viper.("server.port") fmt.Println(host, port) } for _, key := range viper.() { fmt.Println("ключ:", key) }

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

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

Строки

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

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

Карточки: чтение значений

Термин

GetString

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

Читает ключ как строку через cast.ToString: число превратится в текст, отсутствующий ключ даст пустую строку. Вложенные ключи — через точку: server.host.

1 / 6

Что дальше#

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

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · Чтение значений: GetString, GetInt и семейство Get)

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

GetInt читает порт из строки «8080» — это безопасно?

Безопасность тут не в типе, а в границах: cast вызывает strconv.Atoi и молча даёт 0 на мусоре. Для конфига это обычно удобство — деплой-инструменты часто отдают всё строками, — но критичные значения проверяйте границами: порт больше нуля, таймаут не нулевой.

Чем GetStringMap отличается от Get?

GetStringMap сразу даёт map[string]any — вложенную секцию целиком, удобно для динамических деревьев. Get вернул бы то же значение как any, но без типизированного объявления. Оба не отличают отсутствующую секцию от пустой: и там, и там длина ноль.

Можно ли узнать, из какого источника пришло значение — файл, окружение или дефолт?

Прямого API нет: реестр viper не хранит происхождение значений. Практический путь — эксперимент по лестнице приоритетов: закомментируйте ключ в файле, задайте переменную окружения, напечатайте AllSettings и сравните. Диагностика таких случаев — тема урока двадцать второго.

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

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

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

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

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

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

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