[Программирование, Go] JSON с опциональными полями в Go (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Перевод статьи подготовлен специально для будущих студентов курса "Golang Developer. Professional".
Один из наиболее распространенных видов данных, хранящихся в конфигурационных файлах, - это опции. В этой статье я расскажу о некоторых нюансах, которые необходимо учитывать при сохранении опций в JSON и их анмаршалинге в Go.В частности, наиболее важное различие между опциями и любыми другими данными заключается в том, что опции зачастую, извините за каламбур… опциональны. Наша программа может иметь большое количество всевозможных параметров конфигурации (опций), но нам может понадобиться инициировать какую-либо конкретную инвокацию только с ограниченным их подмножеством, оставив для всего остального значения по умолчанию.Основы - частичный анмаршалинг, omitempty и неизвестные поляНачнем с основ. Рассмотрим следующую структуру, которая представляет из себя опции произвольной программы:
type Options struct {
Id string `json:"id,omitempty"`
Verbose bool `json:"verbose,omitempty"`
Level int `json:"level,omitempty"`
Power int `json:"power,omitempty"`
}
Эта структура содержит всего 4 опции, но в реальных программах их могут быть десятки.Предположим, мы хотим указать эти опции в JSON-файле конфигурации. Полный список опций может выглядеть примерно так:
{
"id": "foobar",
"verbose": false,
"level": 10,
"power": 221
}
Если в ваших файлах конфигурации всегда указаны все опции, то говорить дальше особо не о чем. Просто вызовите json.Unmarshal, и дело с концом.На практике же достаточно редко все бывает так просто. Нам следует обработать сразу несколько особых случаев:
- В JSON-конфиге могут отсутствовать некоторые поля, и мы хотим, чтобы наша структура в Go имела для них значения по умолчанию.
- JSON-конфиг может иметь дополнительные поля, которых в нашей структуре нет. В зависимости от обстоятельств мы можем либо проигнорировать их, либо отрапортовать об ошибке.
В случае (1) пакет json Go будет присваивать значения только полям, найденным в JSON; другие поля просто сохранят свои нулевые значения Go. Например, если бы в JSON вообще не было поля level, в анмаршаленной структуре Options Level будет равен 0. Если такое поведение нежелательно для вашей программы, переходите к следующему разделу.В случае (2) пакет json по умолчанию поведет себя вполне терпимо и просто проигнорирует неизвестные поля. То есть предположим, что входной JSON:
{
"id": "foobar",
"bug": 42
}
json.Unmarshal без проблем распарсит это в Options, установив Id в значение "foobar", Level и Power в 0, а Verbose в false. Он проигнорирует поле bug.В одних случаях такое поведение является желательным, в других - нет. К счастью, пакет json позволяет настроить это, предоставляя явную опцию для JSON-декодера в виде DisallowUnknownFields:
dec := json.NewDecoder(bytes.NewReader(jsonText))
dec.DisallowUnknownFields()
var opts Options
if err := dec.Decode(&opts2); err != nil {
fmt.Println("Decode error:", err)
}
Теперь парсинг вышеупомянутого фрагмента JSON приведет к ошибке.Наконец, вы можете заметить, что наша структура Options имеет тег omitempty, указанный для всех полей. Это означает, что поля с нулевыми значениями не будут внесены в JSON. Например:
opts := Options{
Id: "baz",
Level: 0,
}
out, _ := json.MarshalIndent(opts, "", " ")
fmt.Println(string(out))
Выведет:
{
"id": "baz"
}
Потому что все остальные поля имеют нулевые значения. Если вы напротив хотите всегда вносить все поля, не указывайте omitempty.Установка значений по умолчаниюВ приведенном выше примере мы видели, что отсутствующие в JSON-представлении поля будут преобразованы в нулевые значения Go. Это нормально, если значения ваших параметров по умолчанию также являются их нулевыми значениями, что не всегда так. Что, если значение по умолчанию Power должно быть 10, а не 0? То есть, когда JSON не имеет поля «power», вы хотите установить Power равным 10, но вместо этого Unmarshal устанавливает его в ноль.Вы можете подумать - это же элементарно! Я буду устанавливать Power в его значение умолчанию 10 всякий раз, когда он маршалится из JSON как 0! Но подождите. Что произойдет, если в JSON указано значение 0?На самом деле, эта проблема решается наоборот. Мы установим значения по умолчанию сначала, а затем позволим json.Unmarshal перезаписать поля по мере необходимости:
func parseOptions(jsn []byte) Options {
opts := Options{
Verbose: false,
Level: 0,
Power: 10,
}
if err := json.Unmarshal(jsn, &opts); err != nil {
log.Fatal(err)
}
return opts
}
Теперь вместо прямого вызова json.Unmarshal на Options, нам придется вызывать parseOptions.В качестве альтернативы мы можем хитро спрятать эту логику в пользовательском методе UnmarshalJSON для Options:
func (o *Options) UnmarshalJSON(text []byte) error {
type options Options
opts := options{
Power: 10,
}
if err := json.Unmarshal(text, &opts); err != nil {
return err
}
*o = Options(opts)
return nil
}
В этом методе любой вызов json.Unmarshal для типа Options будет заполнять значение по умолчанию Power правильно. Обратите внимание на использование псевдонимного типа options - это нужно для предотвращения бесконечной рекурсии в UnmarshalJSON.Этот подход прост и понятен, но у него есть некоторые недостатки. Во-первых, он прочно связывает значения полей по умолчанию с логикой парсинга. Вполне возможно, что мы хотим, чтобы пользовательский код устанавливал значения по умолчанию позже; прямо сейчас значения по умолчанию нужно установить перед анмаршалингом.Второй недостаток заключается в том, что это работает только для простых случаев. Если наша структура Options содержит срез или мапу других структур, мы не сможем проставить значения по умолчанию таким образом. Примите во внимание:
type Region struct {
Name string `json:"name,omitempty"`
Power int `json:"power,omitempty"`
}
type Options struct {
Id string `json:"id,omitempty"`
Verbose bool `json:"verbose,omitempty"`
Level int `json:"level,omitempty"`
Power int `json:"power,omitempty"`
Regions []Region `json:"regions,omitempty"`
}
Если мы хотим заполнить значения по умолчанию для Power каждой Region, мы не сможем сделать это на уровне Options. Мы должны написать собственный метод анмаршалинга для Region. Это сложно масштабировать для произвольно вложенных структур - распространение нашей логики значений по умолчанию на несколько методов UnmarshalJSON не оптимально.Альтернативой является использование совершенно другого подхода, перекладывая логику значений по умолчанию на пользователя. Мы можем добиться этого с помощью полей-указателей.Значения по умолчанию и поля-указателиМы можем определить нашу структуру Options как:
type Options struct {
Id *string `json:"id,omitempty"`
Verbose *bool `json:"verbose,omitempty"`
Level *int `json:"level,omitempty"`
Power *int `json:"power,omitempty"`
}
Это очень напоминает исходное определение, за исключением того, что все поля теперь являются указателями. Предположим, у нас есть следующий текст JSON:
{
"id": "foobar",
"verbose": false,
"level": 10
}
Обратите внимание, что указаны все поля, кроме "power". Мы можем анмаршалить это как обычно:
var opts Options
if err := json.Unmarshal(jsonText, &opts); err != nil {
log.Fatal(err)
}
Но теперь мы можем четко различить поля, которые не были указаны вообще (они будут анмаршалены в nil указатель), и поля, которые были указаны с нулевыми значениями (они будут анмаршалены в валидные указатели на значения с нулевыми значениями). Например, мы можем написать следующую обертку парсера для анмаршалинга Options и установки значений по умолчанию по мере необходимости:\
func parseOptions(jsn []byte) Options {
var opts Options
if err := json.Unmarshal(jsonText, &opts); err != nil {
log.Fatal(err)
}
if opts.Power == nil {
var v int = 10
opts.Power = &v
}
return opts
}
Обратите внимание, как мы устанавливаем opts.Power; это одно из неудобств работы с указателями, потому что в Go нет синтаксиса, позволяющего принимать адреса литералов встроенных типов, таких как int. Однако это не слишком большая проблема, поскольку существуют простые вспомогательные функции, которые могут сделать нашу жизнь чуть более приятной:
func Bool(v bool) *bool { return &v }
func Int(v int) *int { return &v }
func String(v string) *string { return &v }
// и т.д. ...
Имея это под рукой, мы могли бы просто написать opts.Power = Int(10).Самая полезная особенность этого подхода заключается в том, что он не заставляет нас назначать значения по умолчанию в месте, где парсится JSON. Мы можем передать Options в пользовательский код, и пусть уже он разбирается со значениями по умолчанию, когда ему встречаются поля с nil.Так являются ли указатели волшебным решением нашей проблемы - «отличить неопределенные значения от нулевых значений»? Вроде того. Указатели, безусловно, являются жизнеспособным решением, которое должно хорошо работать. Официальный пакет Protobuf использует тот же подход для protobuf-ов proto2, проводящих различие между необходимыми и опциональными полями. Так что этот метод прошел проверку боем!Тем не менее, это решение не идеально. Прежде всего, несмотря на то, что Go большую часть времени действительно хорошо скрывает дополнительную синтаксическую нагрузку при работе с указателями, в некоторых случаях немного возни все же просачивается (например, получение адреса встроенного литерала, как показано выше). Еще одна потенциальная проблема - производительность. Указатели часто означают динамическое выделение памяти и могут вызывать проблемы с производительностью в некоторых сценариях, хотя, если говорить о структурах параметров, это вряд ли станет проблемой.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Eli Bendersky
===========Похожие новости:
- [Программирование, Разработка под iOS, Облачные сервисы] Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 3 (перевод)
- [Ненормальное программирование, Разработка веб-сайтов, Python, Программирование] iPad для разработчика
- [Программирование, .NET, Разработка под MacOS] Поддержка процессоров Apple M1 в .NET
- [Программирование, Java, Apache, Промышленное программирование] Spring Boot app with Apache Kafka in Docker container (перевод)
- [CMS, PHP, Программирование, Разработка под e-commerce, Интернет-маркетинг] Генератор ocmod-файла для интернет-магазина на Opencart
- [Программирование, Apache, Hadoop] Экономичная конфигурация исполнителей Apache Spark (перевод)
- [Python, Программирование] Functools – Инструменты для работы с функциями (перевод)
- [Open source, Программирование, Системное программирование, Компиляторы, Rust] Rust 1.48.0: упрощение создания ссылок и псевдонимы поиска (перевод)
- [Open source, Google Chrome, Разработка под Linux] Ubuntu Web Remix — альтернатива Chrome OS c браузером Firefox вместо Google Chrome
- [Google App Engine, Облачные вычисления, Google Cloud Platform] Обзор на курс специализации от Coursera — Cloud Architecture with Google Cloud
Теги для поиска: #_programmirovanie (Программирование), #_go, #_go, #_json, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_programmirovanie (
Программирование
), #_go
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:33
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Перевод статьи подготовлен специально для будущих студентов курса "Golang Developer. Professional".
type Options struct {
Id string `json:"id,omitempty"` Verbose bool `json:"verbose,omitempty"` Level int `json:"level,omitempty"` Power int `json:"power,omitempty"` } {
"id": "foobar", "verbose": false, "level": 10, "power": 221 }
{
"id": "foobar", "bug": 42 } dec := json.NewDecoder(bytes.NewReader(jsonText))
dec.DisallowUnknownFields() var opts Options if err := dec.Decode(&opts2); err != nil { fmt.Println("Decode error:", err) } opts := Options{
Id: "baz", Level: 0, } out, _ := json.MarshalIndent(opts, "", " ") fmt.Println(string(out)) {
"id": "baz" } func parseOptions(jsn []byte) Options {
opts := Options{ Verbose: false, Level: 0, Power: 10, } if err := json.Unmarshal(jsn, &opts); err != nil { log.Fatal(err) } return opts } func (o *Options) UnmarshalJSON(text []byte) error {
type options Options opts := options{ Power: 10, } if err := json.Unmarshal(text, &opts); err != nil { return err } *o = Options(opts) return nil } type Region struct {
Name string `json:"name,omitempty"` Power int `json:"power,omitempty"` } type Options struct { Id string `json:"id,omitempty"` Verbose bool `json:"verbose,omitempty"` Level int `json:"level,omitempty"` Power int `json:"power,omitempty"` Regions []Region `json:"regions,omitempty"` } type Options struct {
Id *string `json:"id,omitempty"` Verbose *bool `json:"verbose,omitempty"` Level *int `json:"level,omitempty"` Power *int `json:"power,omitempty"` } {
"id": "foobar", "verbose": false, "level": 10 } var opts Options
if err := json.Unmarshal(jsonText, &opts); err != nil { log.Fatal(err) } func parseOptions(jsn []byte) Options {
var opts Options if err := json.Unmarshal(jsonText, &opts); err != nil { log.Fatal(err) } if opts.Power == nil { var v int = 10 opts.Power = &v } return opts } func Bool(v bool) *bool { return &v }
func Int(v int) *int { return &v } func String(v string) *string { return &v } // и т.д. ... =========== Источник: habr.com =========== =========== Автор оригинала: Eli Bendersky ===========Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_programmirovanie ( Программирование ), #_go |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:33
Часовой пояс: UTC + 5