GoSuda

Анализ на промените в поведението на Go 1.25 encoding/json/v2 и слоя за съвместимост с v1

By lemonmint views ...

Пакетът encoding/json v2 на Go е нова реализация, предназначена да отстрани няколко недостатъка на v1 (липса на последователност, неочаквано поведение, проблеми с производителността). Тя е експериментална функционалност, която се активира чрез build тага goexperiment.jsonv2.

Най-важното е, че когато v2 е активиран, v1 функционира като слой за съвместимост, който емулира поведението на v1 върху реализацията на v2. Това се осъществява чрез функцията DefaultOptionsV1() във файла v2_v1.go. С други думи, v2 предоставя опции, с които поведението на v1 може да се възпроизведе напълно, а едновременно с това въвежда по-строго и предвидимо ново поведение по подразбиране.

Основните цели на v2 са следните:

  1. Подобряване на коректността и предвидимостта: По подразбиране се прилагат по-строги правила (например чувствителност към регистъра и забрана на дублиращи се ключове), за да се намалi неочакваното поведение.
  2. Подобряване на производителността: Машината за парсване и кодиране е преproектирана с цел по-висока ефективност.
  3. Повече гъвкавост и контрол: Въведена е подробна система от опции, която позволява на разработчиците да контролират в детайли обработката на JSON.

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

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

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

  • Поведение в v1: При unmarshal на член на 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 указател/интерфейс или масив/слайс/map/низ с дължина 0.
  • Поведение в v2: Полето се пропуска според "празното състояние" на кодираната JSON стойност. Това означава, че се пропуска, когато стойността се кодира като null, "", {} или [].
  • Причина за промяната: Дефиницията в v1 зависи от системата от типове на Go. v2 се основава на системата от типове на JSON и по този начин осигурява по-последователно поведение. Например в v1 стойността false на bool тип се пропуска, докато в v2 false е непразна JSON стойност и не се пропуска. В v2 е добавена опцията omitzero, която може да замени поведението на omitempty в v1 по отношение на 0 и false.
  • Свързани опции: За да се постигне поведение, идентично с v1, се използва опцията json.OmitEmptyWithLegacyDefinition(true).

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

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

4. Маршалиране на nil слайсове и map-ове

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

5. Unmarshal на масиви

  • Поведение в v1: При unmarshal към Go масив ([N]T) дължината на JSON масива може да не съвпада с дължината на Go масива, без да се предизвиква грешка. Ако JSON масивът е по-къс, останалите елементи се запълват с нулеви стойности, а ако е по-дълъг, излишъкът се отхвърля.
  • Поведение в 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: При маршалиране и unmarshal, ако даден низ съдържа невалидни 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: Набор от опции, които променят поведението при маршалиране и unmarshal.
  • json.JoinOptions(...): Обединява няколко опции в една.
  • WithMarshalers / WithUnmarshalers: Мощна функционалност, която позволява да се инжектира логика за сериализация и десериализация за определени типове, без те да имплементират интерфейсите Marshaler/Unmarshaler. Особено полезна е при работа с типове от външни пакети.
  • Нови опции: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull и други, които позволяват контрол на поведения, невъзможни в v1.

Заключение

encoding/json v2 е съвременна реализация, която на базата на опита с v1 значително подобрява коректността, производителността и гъвкавостта. Поведението по подразбиране стана по-строго, но чрез разширената система от Options всички поведения на v1 могат да се поддържат напълно, така че съвместимостта с наличния код се запазва и предимствата на v2 могат да се въвеждат постепенно.

  • За нови проекти препоръчително е v2 да се използва по подразбиране.
  • Съществуващите проекти могат или да продължат да използват jsonv1 без промени, или да мигрират към jsonv2, като постепенно въвеждат по-строгото поведение на v2 чрез DefaultOptionsV1().