GoSuda

Сравнение `encoding/json` v1 и v2 в Go 1.25

By lemonmint
views ...

Пакет encoding/json v2 в Go представляет собой новую реализацию, направленную на устранение многочисленных недостатков предыдущей версии v1 (таких как отсутствие согласованности, непредсказуемое поведение и проблемы с производительностью). Это экспериментальная функция, которая активируется с помощью тега сборки goexperiment.jsonv2.

Ключевым моментом является то, что при активации v2, v1 функционирует как слой совместимости, эмулирующий поведение v1 поверх реализации v2. Это достигается посредством функции DefaultOptionsV1() в файле v2_v1.go. Таким образом, v2 предоставляет опции для полного воспроизведения поведения v1, одновременно предлагая новые, более строгие и предсказуемые поведения по умолчанию.

Основные цели v2 включают:

  1. Повышение точности и предсказуемости: Применение более строгих правил по умолчанию (например, чувствительность к регистру, запрет дублирующихся ключей) для уменьшения неожиданного поведения.
  2. Улучшение производительности: Перепроектирование механизмов парсинга и кодирования для повышения эффективности.
  3. Расширение гибкости и контроля: Введение детальной системы опций, позволяющей разработчикам тонко настраивать процесс обработки JSON.

Основные различия в семантике/поведении

На основе файла v2_test.go ниже представлены различия в поведении между v1 и v2 по пунктам.

1. Сопоставление имен полей (чувствительность к регистру)

  • Поведение v1: При демаршализации членов JSON-объекта в поля Go-структуры сопоставление выполняется без учета регистра (case-insensitive). Как "FirstName", так и "firstname" будут сопоставлены с полем FirstName.
  • Поведение v2: По умолчанию сопоставление выполняется с учетом регистра (case-sensitive), и сопоставляются только поля, совпадающие точно.
  • Причина изменения: Сопоставление без учета регистра может приводить к неожиданному поведению и снижению производительности при обработке несовпадающих полей. v2 принимает более четкое и предсказуемое поведение по умолчанию.
  • Связанные опции: В v2 можно явно включить сопоставление без учета регистра для отдельных полей с помощью тега json:"...,case:ignore" или применить опцию json.MatchCaseInsensitiveNames(true) глобально.

2. Изменение значения опции тега omitempty

  • Поведение v1: Поле опускается на основе "пустого состояния" Go-значения. Под "пустым состоянием" понимаются false, 0, nil указатели/интерфейсы, массивы/слайсы/карты/строки нулевой длины.
  • Поведение v2: Поле опускается на основе "пустого состояния" закодированного JSON-значения. То есть, оно опускается, если кодируется как null, "", {}, [].
  • Причина изменения: Определение v1 зависело от системы типов Go. v2 предоставляет более согласованное поведение, основываясь на системе типов JSON. Например, в v1 значение false типа bool опускалось, тогда как в v2 false не является пустым JSON-значением и поэтому не опускается. В v2 добавлена опция omitzero, которая может заменить поведение omitempty в v1, применявшееся к 0 или false.
  • Связанные опции: Если в v2 требуется поведение, идентичное v1, используйте опцию json.OmitEmptyWithLegacyDefinition(true).

3. Изменение поведения опции тега string

  • Поведение v1: Применяется к полям числового, булевого и строкового типов. Кодирует соответствующее значение обратно в JSON-строку (например, int(42) -> "42"). Не применяется рекурсивно к значениям внутри составных типов (слайсы, карты и т. д.).
  • Поведение v2: Применяется только к числовым типам и рекурсивно. То есть, числа внутри слайсов, таких как []int, также будут закодированы как JSON-строки.
  • Причина изменения: Основное назначение опции string — представление чисел в виде строк для предотвращения потери точности 64-битных целых чисел. Поведение v1 было ограниченным и непоследовательным. v2 фокусируется на этом основном использовании и рекурсивно расширяет поведение, делая его более полезным.
  • Связанные опции: Опция json.StringifyWithLegacySemantics(true) может имитировать поведение v1.

4. Маршалинг nil слайсов и карт

  • Поведение v1: nil слайсы и nil карты маршалятся как null.
  • Поведение v2: По умолчанию nil слайсы маршалятся как [] (пустой массив), а nil карты — как {} (пустой объект).
  • Причина изменения: nil является деталью реализации языка Go, и его раскрытие в независимом от языка формате JSON нежелательно. [] или {} являются более универсальными представлениями пустых коллекций.
  • Связанные опции: В v2 можно маршалить как null, как в v1, используя опции json.FormatNilSliceAsNull(true) или json.FormatNilMapAsNull(true).

5. Демаршалинг массивов

  • Поведение v1: При демаршалинге в Go-массив ([N]T) не возникает ошибки, даже если длина JSON-массива отличается от длины Go-массива. Если длина короче, оставшееся пространство заполняется нулевыми значениями; если длиннее, избыточные элементы отбрасываются.
  • Поведение v2: Длина JSON-массива должна точно совпадать с длиной Go-массива. В противном случае возникает ошибка.
  • Причина изменения: В Go фиксированные массивы часто имеют важное значение из-за их длины. Поведение v1 могло приводить к молчаливой потере данных. v2 повышает точность с помощью более строгих правил.
  • Связанные опции: Опция json.UnmarshalArrayFromAnyLength(true) может имитировать поведение v1.

6. Обработка time.Duration

  • Поведение v1: time.Duration трактуется как int64 и кодируется как JSON-число в наносекундах.
  • Поведение v2: Используется метод time.Duration.String() для кодирования в JSON-строку формата "1h2m3s".
  • Причина изменения: Числовые наносекунды трудночитаемы, а стандартное строковое представление time.Duration более полезно.
  • Связанные опции: Поведение v1 можно использовать с помощью опции тега json:",format:nano" или опции json.FormatTimeWithLegacySemantics(true).

7. Обработка недействительного UTF-8

  • Поведение v1: При маршалинге/демаршалинге, если в строке присутствуют недействительные байты UTF-8, они молча заменяются символом замены Unicode (\uFFFD).
  • Поведение v2: По умолчанию, при обнаружении недействительного UTF-8, возвращается ошибка.
  • Причина изменения: Предотвращение молчаливого повреждения данных и соблюдение более строгого стандарта JSON (RFC 7493).
  • Связанные опции: Поведение v1 можно имитировать с помощью опции jsontext.AllowInvalidUTF8(true).

8. Обработка дублирующихся имен членов объекта

  • Поведение v1: Допускается наличие дублирующихся членов с одинаковым именем в JSON-объекте. Значение, появившееся последним, перезаписывает предыдущее.
  • Поведение v2: По умолчанию, при наличии дублирующихся имен членов, возвращается ошибка.
  • Причина изменения: Стандарт RFC 8259 не определяет поведение дублирующихся имен, что может приводить к различному поведению в разных реализациях. Это может быть источником уязвимостей безопасности. v2 явно запрещает это, повышая точность и безопасность.
  • Связанные опции: Поведение v1 можно имитировать с помощью опции jsontext.AllowDuplicateNames(true).

Отличия в реализации и архитектуре

  • v1: Сильно зависит от decodeState в decode.go и вручную написанного конечного автомата (state machine) в scanner.go. Это монолитная структура, в которой логика парсинга и семантический анализ тесно связаны.
  • v2: Архитектура более модульная.
    • encoding/json/jsontext: Предоставляет низкоуровневые высокопроизводительные JSON-токенизаторы (Decoder) и кодировщики (Encoder). Этот пакет сосредоточен исключительно на синтаксических аспектах JSON.
    • encoding/json/v2: Основывается на jsontext и обрабатывает семантическое преобразование между Go-типами и JSON-значениями.
    • Такое разделение синтаксического и семантического анализа улучшает ясность кода и производительность.

Новые API и функции v2

v2 предоставляет очень гибкие возможности управления через систему json.Options.

  • json.Options: Набор опций, изменяющих поведение маршалинга/демаршалинга.
  • json.JoinOptions(...): Объединяет несколько опций в одну.
  • WithMarshalers / WithUnmarshalers: Мощная функция, позволяющая внедрять логику сериализации/десериализации для определенных типов без реализации интерфейсов Marshaler/Unmarshaler. Это особенно полезно при работе с типами из внешних пакетов.
  • Новые опции: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull и другие, которые не были доступны в v1, теперь позволяют осуществлять разнообразный контроль над поведением.

Заключение

encoding/json v2 — это современная реализация, значительно улучшающая точность, производительность и гибкость на основе опыта v1. Хотя базовое поведение стало более строгим, продуманная система Options полностью поддерживает все поведения v1, что позволяет постепенно внедрять преимущества v2, сохраняя совместимость с существующим кодом.

  • Для новых проектов рекомендуется использовать v2 по умолчанию.
  • Существующие проекты могут продолжать использовать jsonv1 или мигрировать на jsonv2, используя DefaultOptionsV1() для постепенного внедрения более строгого поведения v2.