GoSuda

Analiza zmian w działaniu encoding/json/v2 w Go 1.25 oraz warstwy zgodności z v1

By lemonmint views ...

Pakiet encoding/json w wersji v2 w Go to nowa implementacja, której celem jest usunięcie kilku wad wersji v1 (brak spójności, zaskakujące zachowania, problemy z wydajnością). Jest to funkcja eksperymentalna, aktywowana za pomocą znacznika kompilacji goexperiment.jsonv2.

Najważniejszy jest fakt, że po aktywacji v2 wersja v1 działa jako warstwa zgodności, która emuluje zachowanie v1 na bazie implementacji v2. Odbywa się to za pomocą funkcji DefaultOptionsV1() z pliku v2_v1.go. Innymi słowy, v2 udostępnia opcje pozwalające w pełni odtworzyć zachowanie v1, a jednocześnie proponuje bardziej rygorystyczne i przewidywalne nowe domyślne zachowanie.

Główne cele v2 są następujące:

  1. Poprawa poprawności i przewidywalności: Domyślnie stosowane są bardziej rygorystyczne reguły (np. rozróżnianie wielkości liter, zakaz zduplikowanych kluczy), co ogranicza nieoczekiwane zachowania.
  2. Poprawa wydajności: Silniki parsowania i kodowania zostały przeprojektowane w celu zwiększenia efektywności.
  3. Zwiększenie elastyczności i kontroli: Wprowadzono szczegółowy system opcji, który pozwala programistom precyzyjnie kontrolować sposób przetwarzania JSON.

Główne różnice w znaczeniu i zachowaniu

Poniżej zestawiono różnice w zachowaniu między v1 a v2 na podstawie pliku v2_test.go.

1. Dopasowywanie nazw pól (rozróżnianie wielkości liter)

  • Zachowanie v1: Podczas unmarshalowania członków obiektu JSON do pól struktury Go dopasowanie odbywa się bez rozróżniania wielkości liter (case-insensitive). Zarówno "FirstName", jak i "firstname" zostaną zmapowane na pole FirstName.
  • Zachowanie v2: Domyślnie dopasowywanie rozróżnia wielkość liter (case-sensitive) i mapuje wyłącznie pola o dokładnie zgodnej nazwie.
  • Uzasadnienie zmiany: Dopasowanie bez rozróżniania wielkości liter może być źródłem nieoczekiwanych zachowań i powodować spadek wydajności przy obsłudze niedopasowanych pól. v2 przyjmuje za domyślne zachowanie jaśniejsze i bardziej przewidywalne.
  • Powiązane opcje: W v2 można jawnie włączyć ignorowanie wielkości liter dla pojedynczego pola za pomocą opcji znacznika json:"...,case:ignore" lub zastosować globalnie opcję json.MatchCaseInsensitiveNames(true).

2. Zmiana znaczenia opcji znacznika omitempty

  • Zachowanie v1: Pole jest pomijane na podstawie „stanu pustego” wartości Go. Oznacza to false, 0, wskaźnik lub interfejs nil, a także tablicę, wycinek, mapę lub łańcuch o długości zero.
  • Zachowanie v2: Pole jest pomijane na podstawie „stanu pustego” zakodowanej wartości JSON. Oznacza to, że pole zostanie pominięte, gdy zostanie zakodowane jako null, "", {} lub [].
  • Uzasadnienie zmiany: Definicja v1 zależy od systemu typów Go. v2 opiera się na systemie typów JSON, zapewniając bardziej spójne zachowanie. Na przykład w v1 wartość false typu bool jest pomijana, natomiast w v2 false jest niepustą wartością JSON i nie zostaje pominięta. W v2 dodano opcję omitzero, która może zastąpić dotychczasowe działanie omitempty dla 0 i false.
  • Powiązane opcje: Aby w v2 uzyskać zachowanie identyczne z v1, należy użyć opcji json.OmitEmptyWithLegacyDefinition(true).

3. Zmiana działania opcji string

  • Zachowanie v1: Dotyczy pól typu liczbowego, logicznego i łańcuchowego. Wartość jest ponownie kodowana wewnątrz łańcucha JSON (np. int(42) -> "42"). Nie jest stosowana rekurencyjnie do wartości wewnątrz typów złożonych (wycinków, map itp.).
  • Zachowanie v2: Opcja działa wyłącznie dla typów liczbowych i jest stosowana rekurencyjnie. Oznacza to, że liczby wewnątrz wycinka, np. []int, również są kodowane jako łańcuchy JSON.
  • Uzasadnienie zmiany: Główne zastosowanie opcji string to zapobieganie utracie precyzji 64-bitowych liczb całkowitych poprzez przedstawianie liczb jako łańcuchów. Zachowanie v1 było ograniczone i niespójne. v2 koncentruje się na tym kluczowym zastosowaniu i rozszerza działanie rekurencyjnie, czyniąc je bardziej użytecznym.
  • Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji json.StringifyWithLegacySemantics(true).

4. Marshalowanie wartości nil dla wycinków i map

  • Zachowanie v1: Wycinki nil i mapy nil są marshalowane jako null.
  • Zachowanie v2: Domyślnie wycinek nil jest marshalowany jako [] (pusta tablica), a mapa nil jako {} (pusty obiekt).
  • Uzasadnienie zmiany: nil jest szczegółem implementacyjnym języka Go, a ujawnianie go w formacie JSON, który nie zależy od języka, nie jest pożądane. Oznaczenia [] i {} stanowią powszechniejszy sposób reprezentacji pustych kolekcji.
  • Powiązane opcje: W v2 można uzyskać marshalowanie jako null, tak jak w v1, używając opcji json.FormatNilSliceAsNull(true) lub json.FormatNilMapAsNull(true).

5. Unmarshalowanie tablic

  • Zachowanie v1: Podczas unmarshalowania do tablicy Go ([N]T) długość tablicy JSON może różnić się od długości tablicy Go bez wywołania błędu. Jeśli tablica JSON jest krótsza, pozostałe miejsca są wypełniane wartościami zerowymi; jeśli jest dłuższa, nadmiarowe elementy są odrzucane.
  • Zachowanie v2: Długość tablicy JSON musi dokładnie odpowiadać długości tablicy Go. W przeciwnym razie zgłaszany jest błąd.
  • Uzasadnienie zmiany: W Go długość tablic o stałym rozmiarze często ma istotne znaczenie. Zachowanie v1 może prowadzić do cichej utraty danych. v2 stosuje bardziej rygorystyczną zasadę, co zwiększa poprawność.
  • Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji json.UnmarshalArrayFromAnyLength(true).

6. Obsługa time.Duration

  • Zachowanie v1: time.Duration jest wewnętrznie traktowany jako int64 i kodowany jako liczba JSON w nanosekundach.
  • Zachowanie v2: Wartość jest kodowana jako łańcuch JSON w formacie takim jak "1h2m3s", z wykorzystaniem metody time.Duration.String().
  • Uzasadnienie zmiany: Liczby w nanosekundach są słabo czytelne, a standardowa reprezentacja łańcuchowa time.Duration jest bardziej użyteczna.
  • Powiązane opcje: Zachowanie v1 można uzyskać za pomocą opcji znacznika json:",format:nano" lub opcji json.FormatTimeWithLegacySemantics(true).

7. Obsługa nieprawidłowego UTF-8

  • Zachowanie v1: Podczas marshalowania i unmarshalowania nieprawidłowe bajty UTF-8 wewnątrz łańcuchów są po cichu zastępowane znakiem zastępczym Unicode (\uFFFD).
  • Zachowanie v2: Domyślnie napotkanie nieprawidłowego UTF-8 powoduje zwrócenie błędu.
  • Uzasadnienie zmiany: Ma to na celu zapobieganie cichemu uszkadzaniu danych oraz zgodność z bardziej rygorystycznym standardem JSON (RFC 7493).
  • Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji jsontext.AllowInvalidUTF8(true).

8. Obsługa zduplikowanych nazw członków obiektu

  • Zachowanie v1: Obiekt JSON może zawierać członków o tej samej nazwie. Ostatnia wartość nadpisuje poprzednie.
  • Zachowanie v2: Domyślnie występowanie zduplikowanej nazwy członka powoduje zwrócenie błędu.
  • Uzasadnienie zmiany: Standard RFC 8259 nie definiuje zachowania dla zduplikowanych nazw, przez co różne implementacje mogą działać odmiennie. Może to być przyczyną luk bezpieczeństwa. v2 jawnie odrzuca takie dane, zwiększając poprawność i bezpieczeństwo.
  • Powiązane opcje: Zachowanie v1 można odtworzyć za pomocą opcji jsontext.AllowDuplicateNames(true).

Różnice w implementacji i architekturze

  • v1: Silnie opiera się na decodeState w pliku decode.go oraz ręcznie napisanej maszynie stanów w scanner.go. Jest to monolityczna struktura, w której logika parsowania jest silnie powiązana z analizą semantyczną.
  • v2: Architektura jest bardziej modularna.
    • encoding/json/jsontext: Udostępnia niskopoziomowy, wydajny tokenizator JSON (Decoder) oraz koder (Encoder). Pakiet ten koncentruje się wyłącznie na składni JSON.
    • encoding/json/v2: Opierając się na jsontext, obsługuje semantyczne przekształcenia między typami Go a wartościami JSON.
    • Dzięki takiemu rozdzieleniu analiza składniowa i semantyczna są od siebie oddzielone, co poprawia czytelność kodu i wydajność.

Nowe API i funkcje v2

v2 zapewnia bardzo elastyczne sterowanie za pomocą systemu json.Options.

  • json.Options: Zbiór opcji zmieniających zachowanie marshalowania i unmarshalowania.
  • json.JoinOptions(...): Łączy wiele opcji w jedną.
  • WithMarshalers / WithUnmarshalers: Potężna funkcja, która pozwala wstrzyknąć logikę serializacji i deserializacji dla konkretnego typu bez implementowania interfejsów Marshaler i Unmarshaler. Jest szczególnie przydatna przy obsłudze typów z zewnętrznych pakietów.
  • Nowe opcje: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull i inne, które umożliwiają kontrolę zachowania niedostępną w v1.

Podsumowanie

encoding/json v2 to nowoczesna implementacja, która na podstawie doświadczeń z v1 znacząco poprawia poprawność, wydajność i elastyczność. Domyślne zachowanie stało się bardziej rygorystyczne, jednak dzięki zaawansowanemu systemowi Options możliwe jest w pełni odtworzenie wszystkich zachowań v1, co pozwala zachować zgodność z istniejącym kodem i stopniowo wdrażać zalety v2.

  • Nowe projekty powinny domyślnie korzystać z v2.
  • Istniejące projekty mogą nadal używać jsonv1 bez zmian lub migrować do jsonv2, stosując DefaultOptionsV1() i stopniowo wprowadzać rygorystyczne zachowanie v2.