GoSuda

Comparație între Go 1.25 encoding/json v1 și v2

By lemonmint
views ...

Pachetul encoding/json v2 din Go reprezintă o nouă implementare menită să amelioreze multiple deficiențe ale versiunii v1 (lipsa de consistență, comportamente surprinzătoare, probleme de performanță). Aceasta este o funcționalitate experimentală, activată prin intermediul tag-ului de compilare goexperiment.jsonv2.

Aspectul cel mai important este că atunci când v2 este activată, v1 funcționează ca un strat de compatibilitate care emulează comportamentul v1 peste implementarea v2. Acest lucru este realizat prin funcția DefaultOptionsV1() din fișierul v2_v1.go. Aceasta înseamnă că v2 oferă opțiuni pentru a reproduce perfect comportamentul v1, prezentând în același timp un nou comportament implicit, mai strict și mai predictibil.

Principalele obiective ale v2 sunt următoarele:

  1. Îmbunătățirea preciziei și predictibilității: Prin aplicarea implicită a unor reguli mai stricte (de exemplu, sensibilitatea la majuscule/minuscule, interzicerea cheilor duplicate), se reduc comportamentele neașteptate.
  2. Îmbunătățirea performanței: Motoarele de parsare și codificare au fost reproiectate pentru a crește eficiența.
  3. Extinderea flexibilității și controlului: A fost introdus un sistem detaliat de opțiuni, permițând dezvoltatorilor să controleze fin modul de prelucrare a JSON-ului.

Diferențe majore de semantică/comportament

Diferențele de comportament dintre v1 și v2 au fost organizate pe categorii, având ca punct central fișierul v2_test.go.

1. Potrivirea numelui câmpului (sensibilitate la majuscule/minuscule)

  • Comportament v1: La unmarshaling-ul membrilor obiectelor JSON în câmpurile structurilor Go, potrivirea se realizează fără a ține cont de majuscule/minuscule (case-insensitive). Atât "FirstName", cât și "firstname" sunt mapate la câmpul FirstName.
  • Comportament v2: Implicit, potrivirea se realizează ținând cont de majuscule/minuscule (case-sensitive), mapând doar câmpurile care se potrivesc exact.
  • Motivul modificării: Potrivirea fără sensibilitate la majuscule/minuscule poate fi o sursă de comportamente neașteptate și poate duce la degradarea performanței la procesarea câmpurilor care nu se potrivesc. V2 a adoptat implicit un comportament mai clar și mai predictibil.
  • Opțiuni relevante: În v2, se poate utiliza opțiunea de tag json:"...,case:ignore" pentru a activa explicit potrivirea fără sensibilitate la majuscule/minuscule per câmp, sau se poate aplica opțiunea json.MatchCaseInsensitiveNames(true) la nivel global.

2. Modificarea semnificației opțiunii de tag omitempty

  • Comportament v1: Omite câmpurile pe baza stării "goale" a valorii Go. Prin "stare goală" se înțelege false, 0, pointeri/interfețe nil, array-uri/slice-uri/mape/șiruri de caractere cu lungimea zero.
  • Comportament v2: Omite câmpurile pe baza stării "goale" a valorii JSON codificate. Adică, sunt omise dacă sunt codificate ca null, "", {}, [].
  • Motivul modificării: Definiția v1 este dependentă de sistemul de tipuri Go. V2 oferă un comportament mai consistent bazat pe sistemul de tipuri JSON. De exemplu, în v1, valoarea false a tipului bool este omisă, dar în v2, false nu este o valoare JSON goală, deci nu este omisă. V2 a adăugat opțiunea omitzero pentru a înlocui comportamentul omitempty din v1 aplicat la 0 sau false.
  • Opțiuni relevante: Dacă se dorește același comportament ca în v1 în v2, se utilizează opțiunea json.OmitEmptyWithLegacyDefinition(true).

3. Modificarea comportamentului opțiunii de tag string

  • Comportament v1: Se aplică câmpurilor de tip numeric, boolean și șir de caractere. Valoarea respectivă este re-codificată într-un șir JSON (de exemplu: int(42) -> "42"). Nu se aplică recursiv valorilor din tipuri compuse (slice-uri, mape etc.).
  • Comportament v2: Se aplică doar tipurilor numerice și se aplică recursiv. Adică, chiar și numerele din interiorul slice-urilor, cum ar fi []int, sunt codificate ca șiruri JSON.
  • Motivul modificării: Scopul principal al opțiunii string este de a reprezenta numerele ca șiruri pentru a preveni pierderea preciziei în cazul numerelor întregi pe 64 de biți. Comportamentul v1 era limitat și inconsistent. V2 se concentrează pe această utilizare esențială și extinde comportamentul recursiv pentru a-l face mai util.
  • Opțiuni relevante: Se poate simula comportamentul v1 cu opțiunea json.StringifyWithLegacySemantics(true).

4. Marshalling-ul slice-urilor și map-urilor nil

  • Comportament v1: Slice-urile nil și map-urile nil sunt marshalate ca null.
  • Comportament v2: Implicit, slice-urile nil sunt marshalate ca [] (array gol), iar map-urile nil ca {} (obiect gol).
  • Motivul modificării: nil este un detaliu de implementare al limbajului Go, și expunerea acestuia în formatul JSON, independent de limbaj, nu este de dorit. [] sau {} sunt reprezentări mai universale pentru colecțiile goale.
  • Opțiuni relevante: În v2, se poate marshall-a ca null, similar cu v1, prin opțiunile json.FormatNilSliceAsNull(true) sau json.FormatNilMapAsNull(true).

5. Unmarshaling-ul array-urilor

  • Comportament v1: La unmarshaling-ul într-un array Go ([N]T), nu se generează eroare chiar dacă lungimea array-ului JSON este diferită de lungimea array-ului Go. Dacă lungimea este mai scurtă, spațiul rămas este umplut cu valori zero; dacă este mai lungă, excesul este ignorat.
  • Comportament v2: Lungimea array-ului JSON trebuie să se potrivească exact cu lungimea array-ului Go. Altfel, se generează o eroare.
  • Motivul modificării: În Go, array-urile de dimensiune fixă au adesea o semnificație importantă în ceea ce privește lungimea lor. Comportamentul v1 putea duce la pierderi silențioase de date. V2 a sporit precizia prin reguli mai stricte.
  • Opțiuni relevante: Se poate simula comportamentul v1 cu opțiunea json.UnmarshalArrayFromAnyLength(true).

6. Tratarea time.Duration

  • Comportament v1: time.Duration este tratat intern ca un int64 și codificat ca un număr JSON în nanosecunde.
  • Comportament v2: Este codificat ca un șir JSON de forma "1h2m3s" folosind metoda time.Duration.String().
  • Motivul modificării: Nanosecundele numerice au o lizibilitate redusă, iar reprezentarea standard a șirului pentru time.Duration este mai utilă.
  • Opțiuni relevante: Se poate utiliza comportamentul v1 prin opțiunea de tag json:",format:nano" sau json.FormatTimeWithLegacySemantics(true).

7. Tratarea UTF-8 invalid

  • Comportament v1: La marshalling/unmarshaling, dacă există octeți UTF-8 invalizi într-un șir, aceștia sunt înlocuiți silențios cu caracterul de înlocuire Unicode (\uFFFD).
  • Comportament v2: Implicit, dacă se întâlnește UTF-8 invalid, se returnează o eroare.
  • Motivul modificării: Pentru a preveni deteriorarea silențioasă a datelor și pentru a respecta standardul JSON mai strict (RFC 7493).
  • Opțiuni relevante: Se poate simula comportamentul v1 cu opțiunea jsontext.AllowInvalidUTF8(true).

8. Tratarea numelor de membri de obiect duplicate

  • Comportament v1: Permite apariția duplicată a membrilor cu același nume într-un obiect JSON. Valoarea care apare ultima o suprascrie pe cea anterioară.
  • Comportament v2: Implicit, dacă există nume de membri duplicate, se returnează o eroare.
  • Motivul modificării: Standardul RFC 8259 nu definește comportamentul numelor duplicate, ceea ce poate duce la comportamente diferite în diverse implementări. Acest lucru poate fi o sursă de vulnerabilități de securitate. V2 refuză explicit acest lucru pentru a crește precizia și securitatea.
  • Opțiuni relevante: Se poate simula comportamentul v1 cu opțiunea jsontext.AllowDuplicateNames(true).

Diferențe de implementare și arhitectură

  • v1: Depinde în mare măsură de decodeState din decode.go și de o mașină de stări (state machine) scrisă manual în scanner.go. Aceasta este o structură monolitică în care logica de parsare și analiza semantică sunt puternic cuplate.
  • v2: Arhitectura este mai modularizată.
    • encoding/json/jsontext: Oferă un tokenizer JSON (Decoder) și un encoder (Encoder) de nivel scăzut, de înaltă performanță. Acest pachet se concentrează exclusiv pe aspectele sintactice ale JSON-ului.
    • encoding/json/v2: Gestionează transformările semantice între tipurile Go și valorile JSON, bazându-se pe jsontext.
    • Această separare a analizei sintactice și semantice îmbunătățește claritatea codului și performanța.

API-uri și funcționalități noi în v2

V2 oferă funcționalități de control extrem de flexibile prin sistemul json.Options.

  • json.Options: Un set de opțiuni care modifică comportamentul de marshalling/unmarshaling.
  • json.JoinOptions(...): Combină mai multe opțiuni într-una singură.
  • WithMarshalers / WithUnmarshalers: O funcționalitate puternică care permite injectarea logicii de serializare/deserializare pentru un anumit tip, chiar și fără implementarea interfețelor Marshaler/Unmarshaler. Acest lucru este deosebit de util la procesarea tipurilor din pachete externe.
  • Opțiuni noi: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull și multe altele, care permit un control variat al comportamentului, imposibil în v1.

Concluzie

encoding/json v2 este o implementare modernă care îmbunătățește semnificativ precizia, performanța și flexibilitatea, bazându-se pe experiența v1. Deși comportamentul implicit este mai strict, sistemul sofisticat de Options suportă pe deplin toate comportamentele v1, permițând adoptarea treptată a avantajelor v2, menținând în același timp compatibilitatea cu codul existent.

  • Pentru proiecte noi, este recomandat să se utilizeze v2 implicit.
  • Proiectele existente pot continua să utilizeze jsonv1 sau pot migra la jsonv2, adoptând treptat comportamentul strict al v2 prin DefaultOptionsV1().