GoSuda

Сравнение на Go 1.25 encoding/json v1 срещу v2

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.

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

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

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 указател/интерфейс, масив/слайс/карта/низ с дължина 0.
  • Поведение на 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).
  • Свързани опции: Опцията jsontext.AllowInvalidUTF8(true) може да имитира поведението на v1.

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

  • Поведение на v1: Позволява дублиране на членове с едно и също име в JSON обект. Последната стойност презаписва предишните.
  • Поведение на v2: По подразбиране, ако има дублиращи се имена на членове, се връща грешка.
  • Причина за промяната: Стандартът RFC 8259 не дефинира поведението на дублиращите се имена, което може да доведе до различно поведение в различните имплементации. Това може да е източник на уязвимости в сигурността. V2 изрично отхвърля това, за да повиши точността и сигурността.
  • Свързани опции: Опцията jsontext.AllowDuplicateNames(true) може да имитира поведението на v1.

Разлики в имплементацията и архитектурата

  • 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, като използват стратегия за постепенно въвеждане на стриктното поведение на v2 чрез DefaultOptionsV1().