GoSuda

Analyse af adfærdsændringerne i Go 1.25 encoding/json/v2 og v1-kompatibilitetslaget

By lemonmint views ...

Go-pakken encoding/json v2 er en ny implementering, der har til formål at forbedre flere ulemper ved den eksisterende v1 (manglende konsistens, overraskende adfærd, ydelsesproblemer). Den er en eksperimentel funktion, som aktiveres via build-tagget goexperiment.jsonv2.

Det vigtigste punkt er, at når v2 er aktiveret, fungerer v1 som et kompatibilitetslag, der emulerer v1's adfærd oven på v2-implementeringen. Dette sker via funktionen DefaultOptionsV1() i filen v2_v1.go. Med andre ord tilbyder v2 muligheden for at reproducere v1's adfærd fuldt ud, samtidig med at den introducerer nye standardadfærd, der er strengere og mere forudsigelig.

Hovedmålene for v2 er følgende:

  1. Forbedret korrekthed og forudsigelighed: Som standard anvendes strengere regler (f.eks. versalfølsomhed og forbud mod dublerede nøgler) for at reducere uventet adfærd.
  2. Forbedret ydelse: Parsing- og kodningsmotoren er redesignet for at øge effektiviteten.
  3. Større fleksibilitet og kontrol: Et detaljeret optionssystem introduceres, så udviklere kan styre JSON-behandlingen præcist.

Væsentlige forskelle i betydning og adfærd

Med udgangspunkt i filen v2_test.go er forskellene i adfærd mellem v1 og v2 opstillet punkt for punkt.

1. Matchning af feltnavne (versalfølsomhed)

  • v1-adfærd: Ved unmarshaling af JSON-objektmedlemmer til Go-structfelter matches der uden hensyn til versaler (case-insensitive). Både "FirstName" og "firstname" mappes til feltet FirstName.
  • v2-adfærd: Som standard er matchningen versalfølsom (case-sensitive), og kun felter, der matcher præcist, mappes.
  • Begrundelse for ændringen: Matchning uden hensyn til versaler kan være årsag til uventet adfærd og forårsager ydelsestab ved håndtering af felter, der ikke matcher. v2 har valgt en tydeligere og mere forudsigelig adfærd som standard.
  • Relevante optioner: I v2 kan man eksplicit aktivere versaluafhængig matchning for enkelte felter med tag-optionen json:"...,case:ignore", eller anvende json.MatchCaseInsensitiveNames(true) globalt.

2. Ændret betydning af tag-optionen omitempty

  • v1-adfærd: Felter udelades ud fra Go-værdiens "tomme tilstand". Med "tom tilstand" menes false, 0, nil-pointere/-interfaces samt arrays/slices/maps/strenge med længden 0.
  • v2-adfærd: Felter udelades ud fra den kodede JSON-værdis "tomme tilstand". Det vil sige, at felter udelades, når de kodes til null, "", {} eller [].
  • Begrundelse for ændringen: v1's definition er afhængig af Go's typesystem. v2 tager udgangspunkt i JSON's typesystem og giver dermed en mere konsistent adfærd. For eksempel udelades værdien false af type bool i v1, men i v2 udelades false ikke, da det er en ikke-tom JSON-værdi. I v2 er optionen omitzero tilføjet, så den kan erstatte v1's adfærd, hvor omitempty gjaldt for 0 og false.
  • Relevante optioner: Ønsker man samme adfærd som v1 i v2, anvendes json.OmitEmptyWithLegacyDefinition(true).

3. Ændret adfærd for tag-optionen string

  • v1-adfærd: Gælder for felter af typen tal, boolean og streng. Værdien kodes igen ind i en JSON-streng (f.eks. int(42) -> "42"). Værdier inden i sammensatte typer (slices, maps osv.) påvirkes ikke rekursivt.
  • v2-adfærd: Gælder kun for talttyper, og anvendes rekursivt. Det betyder, at tal inden i en slice som []int også kodes til JSON-strenge.
  • Begrundelse for ændringen: Hovedformålet med optionen string er at forhindre præcisionstab ved 64-bit heltal ved at repræsentere tal som strenge. v1's adfærd var begrænset og manglede konsistens. v2 fokuserer på dette kernebehov og udvider adfærden rekursivt for at gøre den mere nyttig.
  • Relevante optioner: Med json.StringifyWithLegacySemantics(true) kan v1's adfærd efterlignes.

4. Marshaling af nil-slices og -maps

  • v1-adfærd: nil-slices og nil-maps marshales til null.
  • v2-adfærd: Som standard marshales nil-slices til [] (tomt array) og nil-maps til {} (tomt objekt).
  • Begrundelse for ændringen: nil er en implementeringsdetalje i sproget Go, og det er uhensigtsmæssigt at eksponere den i et sprogneutralt JSON-format. [] og {} er mere udbredte repræsentationer af tomme samlinger.
  • Relevante optioner: I v2 kan man med json.FormatNilSliceAsNull(true) eller json.FormatNilMapAsNull(true) marshale til null ligesom i v1.

5. Unmarshaling af arrays

  • v1-adfærd: Ved unmarshaling til et Go-array ([N]T) forårsager det ingen fejl, hvis længden af JSON-arrayet afviger fra længden af Go-arrayet. Er JSON-arrayet kortere, udfyldes de resterende pladser med nulværdier, og er det længere, kasseres overskydende elementer.
  • v2-adfærd: Længden af JSON-arrayet skal præcis svare til længden af Go-arrayet. Ellers opstår der en fejl.
  • Begrundelse for ændringen: I Go har faste arraystørrelser ofte en betydning, der afhænger af længden. v1's adfærd kan føre til lydløst datatab. v2 indfører strengere regler for at øge korrektheden.
  • Relevante optioner: Med json.UnmarshalArrayFromAnyLength(true) kan v1's adfærd efterlignes.

6. Håndtering af time.Duration

  • v1-adfærd: time.Duration behandles internt som int64 og kodes som et JSON-tal i nanosekunder.
  • v2-adfærd: Kodes som en JSON-streng i formatet "1h2m3s" ved hjælp af metoden time.Duration.String().
  • Begrundelse for ændringen: Tal i nanosekunder er svære at læse, og time.Duration's standardstrengrepræsentation er mere anvendelig.
  • Relevante optioner: Med tag-optionen json:",format:nano" eller optionen json.FormatTimeWithLegacySemantics(true) kan v1's adfærd anvendes.

7. Håndtering af ugyldig UTF-8

  • v1-adfærd: Ved marshaling/unmarshaling erstattes ugyldige UTF-8-bytes i strenge lydløst med Unicode-erstatningstegnet (\uFFFD).
  • v2-adfærd: Som standard returneres en fejl, når der stødes på ugyldig UTF-8.
  • Begrundelse for ændringen: Formålet er at forhindre lydløs datakorruption og at overholde den strengere JSON-standard (RFC 7493).
  • Relevante optioner: Med jsontext.AllowInvalidUTF8(true) kan v1's adfærd efterlignes.

8. Håndtering af dublerede objektmedlemsnavne

  • v1-adfærd: Et JSON-objekt må indeholde medlemmer med samme navn. Den sidst forekommende værdi overskriver de tidligere.
  • v2-adfærd: Som standard returneres en fejl, hvis der findes dublerede medlemsnavne.
  • Begrundelse for ændringen: RFC 8259 definerer ikke adfærden for dublerede navne, hvilket kan give forskellig adfærd mellem implementeringer. Dette kan være årsag til sikkerhedssårbarheder. v2 afviser dem eksplicit for at øge korrektheden og sikkerheden.
  • Relevante optioner: Med jsontext.AllowDuplicateNames(true) kan v1's adfærd efterlignes.

Forskelle i implementering og arkitektur

  • v1: Er i høj grad afhængig af decodeState i decode.go og den manuelt skrevne tilstandsmaskine (state machine) i scanner.go. Det er en monolitisk struktur, hvor parsinglogik og semantisk analyse er tæt koblet.
  • v2: Arkitekturen er mere modulær.
    • encoding/json/jsontext: Leverer en lavniveau-tokenizer (Decoder) og encoder (Encoder) med høj ydelse. Pakken fokuserer udelukkende på JSON's syntaktiske aspekter.
    • encoding/json/v2: Bygger på jsontext og håndterer den semantiske konvertering mellem Go-typer og JSON-værdier.
    • Ved denne opdeling adskilles syntaktisk og semantisk analyse, hvilket forbedrer kodens klarhed og ydelse.

Nye API'er og funktioner i v2

v2 tilbyder meget fleksibel styring via optionssystemet json.Options.

  • json.Options: En samling af optioner, der ændrer adfærden ved marshaling/unmarshaling.
  • json.JoinOptions(...): Sammenfletter flere optioner til én.
  • WithMarshalers / WithUnmarshalers: En kraftfuld funktion, der gør det muligt at indsætte serialiserings- og deserialiseringslogik for en bestemt type uden at implementere grænsefladerne Marshaler/Unmarshaler. Den er særligt nyttig ved håndtering af typer fra eksterne pakker.
  • Nye optioner: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull m.fl. gør det muligt at styre adfærden på måder, der ikke var mulige i v1.

Konklusion

encoding/json v2 er en moderne implementering, der bygger på erfaringerne fra v1 og væsentligt forbedrer korrekthed, ydelse og fleksibilitet. Standardadfærden er blevet strengere, men via det avancerede Options-system understøtter v2 fuldt ud alle v1's adfærdsmønstre. Dermed kan eksisterende kode bevares kompatibel, mens man gradvist tager v2's fordele i brug.

  • Nye projekter bør som udgangspunkt anvende v2.
  • Eksisterende projekter kan enten fortsætte med jsonv1 uændret, eller migrere til jsonv2 og gradvist indføre v2's strengere adfærd via DefaultOptionsV1().