GoSuda

Go 1.25 encoding/json v1 vs v2 Vergleich

By lemonmint
views ...

Die encoding/json-Paket v2 von Go ist eine neue Implementierung, die darauf abzielt, verschiedene Mängel der bestehenden v1 (mangelnde Konsistenz, überraschendes Verhalten, Leistungsprobleme) zu verbessern. Es handelt sich um eine experimentelle Funktion, die über den Build-Tag goexperiment.jsonv2 aktiviert wird.

Am wichtigsten ist, dass v1, wenn v2 aktiviert ist, als Kompatibilitätsschicht fungiert, die das Verhalten von v1 über der v2-Implementierung emuliert. Dies wird durch die Funktion DefaultOptionsV1() in der Datei v2_v1.go erreicht. Das bedeutet, v2 bietet Optionen, um das Verhalten von v1 vollständig zu reproduzieren, während es gleichzeitig ein neues, strengeres und vorhersagbareres Standardverhalten einführt.

Die Hauptziele von v2 sind:

  1. Verbesserung der Genauigkeit und Vorhersagbarkeit: Durch die Anwendung strengerer Regeln (z. B. Groß-/Kleinschreibung, Verbot doppelter Schlüssel) werden unerwartete Verhaltensweisen reduziert.
  2. Leistungsverbesserung: Das Parsing- und Encoding-Engine wurde neu gestaltet, um die Effizienz zu steigern.
  3. Erweiterte Flexibilität und Kontrolle: Ein detailliertes Optionssystem wurde eingeführt, das Entwicklern eine feine Kontrolle über die JSON-Verarbeitung ermöglicht.

Wesentliche Bedeutungs-/Verhaltensunterschiede

Die Verhaltensunterschiede zwischen v1 und v2 wurden, hauptsächlich basierend auf der Datei v2_test.go, nach Elementen geordnet.

1. Feldnamen-Matching (Groß-/Kleinschreibung)

  • v1-Verhalten: Beim Unmarshalling von JSON-Objektmitgliedern in Go-Strukturfelder erfolgt das Matching ohne Berücksichtigung der Groß-/Kleinschreibung (case-insensitive). Sowohl "FirstName" als auch "firstname" werden dem Feld FirstName zugeordnet.
  • v2-Verhalten: Standardmäßig erfolgt das Matching unter Berücksichtigung der Groß-/Kleinschreibung (case-sensitive), wobei nur exakt übereinstimmende Felder zugeordnet werden.
  • Grund der Änderung: Das Matching ohne Berücksichtigung der Groß-/Kleinschreibung kann zu unerwartetem Verhalten führen und eine Leistungseinbuße verursachen, wenn nicht übereinstimmende Felder verarbeitet werden. v2 hat ein klareres und vorhersagbareres Verhalten als Standard übernommen.
  • Verwandte Optionen: In v2 kann die Groß-/Kleinschreibung pro Feld explizit über die Tag-Option json:"...,case:ignore" aktiviert oder die Option json.MatchCaseInsensitiveNames(true) global angewendet werden.

2. Geänderte Bedeutung der omitempty-Tag-Option

  • v1-Verhalten: Felder werden basierend auf dem "leeren Zustand" des Go-Werts weggelassen. "Leerer Zustand" bedeutet hier false, 0, nil-Pointer/Interface, oder Arrays/Slices/Maps/Strings der Länge 0.
  • v2-Verhalten: Felder werden basierend auf dem "leeren Zustand" des enkodierten JSON-Werts weggelassen. Das heißt, sie werden weggelassen, wenn sie als null, "", {}, [] enkodiert werden.
  • Grund der Änderung: Die Definition von v1 ist an das Typensystem von Go gebunden. v2 bietet ein konsistenteres Verhalten, basierend auf dem JSON-Typensystem. Zum Beispiel wird in v1 der false-Wert eines bool-Typs weggelassen, während in v2 false kein leerer JSON-Wert ist und daher nicht weggelassen wird. In v2 kann die Option omitzero hinzugefügt werden, um das Verhalten von v1 zu ersetzen, bei dem omitempty auf 0 oder false angewendet wurde.
  • Verwandte Optionen: Wenn in v2 das gleiche Verhalten wie in v1 gewünscht wird, verwenden Sie die Option json.OmitEmptyWithLegacyDefinition(true).

3. Geändertes Verhalten der string-Tag-Option

  • v1-Verhalten: Wird auf numerische, boolesche und String-Typ-Felder angewendet. Der Wert wird erneut in einen JSON-String enkodiert (z. B. int(42) -> "42"). Werte innerhalb komplexer Typen (Slices, Maps usw.) werden nicht rekursiv angewendet.
  • v2-Verhalten: Wird nur auf numerische Typen angewendet und rekursiv. Das heißt, auch Zahlen innerhalb von Slices wie []int werden alle als JSON-Strings enkodiert.
  • Grund der Änderung: Der Hauptzweck der string-Option ist die Darstellung von Zahlen als Strings, um den Verlust der Präzision bei 64-Bit-Ganzzahlen zu verhindern. Das Verhalten von v1 war begrenzt und inkonsistent. v2 konzentriert sich auf diesen Kernzweck und erweitert das Verhalten rekursiv, um es nützlicher zu machen.
  • Verwandte Optionen: Das Verhalten von v1 kann mit der Option json.StringifyWithLegacySemantics(true) nachgeahmt werden.

4. Marshalling von nil-Slices und -Maps

  • v1-Verhalten: nil-Slices und nil-Maps werden als null gemarshallt.
  • v2-Verhalten: Standardmäßig werden nil-Slices als [] (leeres Array) und nil-Maps als {} (leeres Objekt) gemarshallt.
  • Grund der Änderung: nil ist ein Implementierungsdetail der Go-Sprache, und es ist nicht wünschenswert, es in ein sprachunabhängiges JSON-Format zu exportieren. [] oder {} sind gebräuchlichere Darstellungen für leere Kollektionen.
  • Verwandte Optionen: In v2 kann durch die Optionen json.FormatNilSliceAsNull(true) oder json.FormatNilMapAsNull(true) das Marshalling als null wie in v1 erfolgen.

5. Array-Unmarshalling

  • v1-Verhalten: Beim Unmarshalling in Go-Arrays ([N]T) wird kein Fehler ausgelöst, auch wenn die Länge des JSON-Arrays von der Länge des Go-Arrays abweicht. Wenn die Länge kürzer ist, werden die verbleibenden Felder mit Nullwerten gefüllt; wenn sie länger ist, werden die überschüssigen Werte verworfen.
  • v2-Verhalten: Die Länge des JSON-Arrays muss exakt mit der Länge des Go-Arrays übereinstimmen. Andernfalls wird ein Fehler ausgelöst.
  • Grund der Änderung: In Go haben Arrays fester Größe oft eine wichtige Bedeutung hinsichtlich ihrer Länge. Das Verhalten von v1 konnte zu einem stillen Datenverlust führen. v2 erhöht die Genauigkeit durch strengere Regeln.
  • Verwandte Optionen: Das Verhalten von v1 kann mit der Option json.UnmarshalArrayFromAnyLength(true) nachgeahmt werden.

6. time.Duration-Verarbeitung

  • v1-Verhalten: time.Duration wird intern als int64 behandelt und als JSON-Zahl in Nanosekunden enkodiert.
  • v2-Verhalten: Wird unter Verwendung der time.Duration.String()-Methode als JSON-String im Format "1h2m3s" enkodiert.
  • Grund der Änderung: Numerische Nanosekunden sind schlecht lesbar, und die Standard-String-Darstellung von time.Duration ist nützlicher.
  • Verwandte Optionen: Das Verhalten von v1 kann über die Tag-Option json:",format:nano" oder die Option json.FormatTimeWithLegacySemantics(true) verwendet werden.

7. Umgang mit ungültigem UTF-8

  • v1-Verhalten: Beim Marshalling/Unmarshalling werden ungültige UTF-8-Bytes in einem String stillschweigend durch das Unicode-Ersatzzeichen (\uFFFD) ersetzt.
  • v2-Verhalten: Standardmäßig wird ein Fehler zurückgegeben, wenn ungültiges UTF-8 angetroffen wird.
  • Grund der Änderung: Dies dient dazu, stillschweigende Datenkorruption zu verhindern und dem strengeren JSON-Standard (RFC 7493) zu entsprechen.
  • Verwandte Optionen: Das Verhalten von v1 kann mit der Option jsontext.AllowInvalidUTF8(true) nachgeahmt werden.

8. Umgang mit doppelten Objektnamen

  • v1-Verhalten: Erlaubt das doppelte Auftreten von Membern mit demselben Namen innerhalb eines JSON-Objekts. Der zuletzt erschienene Wert überschreibt frühere.
  • v2-Verhalten: Standardmäßig wird ein Fehler zurückgegeben, wenn doppelte Member-Namen vorhanden sind.
  • Grund der Änderung: Der RFC 8259-Standard definiert das Verhalten bei doppelten Namen nicht, was zu unterschiedlichem Verhalten je nach Implementierung führen kann. Dies kann eine Ursache für Sicherheitslücken sein. v2 lehnt dies explizit ab, um die Genauigkeit und Sicherheit zu erhöhen.
  • Verwandte Optionen: Das Verhalten von v1 kann mit der Option jsontext.AllowDuplicateNames(true) nachgeahmt werden.

Implementierungs- und Architekturunterschiede

  • v1: Stark abhängig von decodeState in decode.go und einer manuell geschriebenen State Machine in scanner.go. Dies ist eine monolithische Struktur, bei der die Parsing-Logik und die semantische Analyse stark gekoppelt sind.
  • v2: Die Architektur ist modularer aufgebaut.
    • encoding/json/jsontext: Bietet einen Low-Level-High-Performance-JSON-Tokenisierer (Decoder) und -Encoder (Encoder). Dieses Paket konzentriert sich ausschließlich auf die syntaktischen Aspekte von JSON.
    • encoding/json/v2: Basiert auf jsontext und behandelt die semantische Konvertierung zwischen Go-Typen und JSON-Werten.
    • Diese Trennung von Syntax- und Semantikanalyse verbessert die Codeklarheit und Leistung.

Neue API und Funktionen in v2

v2 bietet durch das json.Options-System eine sehr flexible Steuerungsfunktion.

  • json.Options: Eine Sammlung von Optionen, die das Marshalling-/Unmarshalling-Verhalten ändern.
  • json.JoinOptions(...): Fasst mehrere Optionen zu einer zusammen.
  • WithMarshalers / WithUnmarshalers: Eine leistungsstarke Funktion, die das Injizieren von Serialisierungs-/Deserialisierungslogik für bestimmte Typen ermöglicht, ohne dass die Marshaler-/Unmarshaler-Interfaces implementiert werden müssen. Dies ist besonders nützlich bei der Verarbeitung von Typen aus externen Paketen.
  • Neue Optionen: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull und viele andere Verhaltenssteuerungen, die in v1 nicht möglich waren, sind nun verfügbar.

Fazit

encoding/json v2 ist eine moderne Implementierung, die auf den Erfahrungen von v1 aufbaut und Genauigkeit, Leistung und Flexibilität erheblich verbessert. Obwohl das Standardverhalten strenger geworden ist, unterstützt das ausgefeilte Options-System alle Verhaltensweisen von v1 vollständig, sodass die Vorteile von v2 schrittweise eingeführt werden können, während die Kompatibilität mit bestehendem Code erhalten bleibt.

  • Für neue Projekte wird empfohlen, v2 standardmäßig zu verwenden.
  • Bestehende Projekte können entweder jsonv1 weiterhin verwenden oder eine Strategie verfolgen, bei der sie zu jsonv2 migrieren und das strengere Verhalten von v2 schrittweise über DefaultOptionsV1() einführen.