JSON-serialisatie met Golang

Overzicht

JSON is een van de populairste serialisatieformaten. Het is door mensen leesbaar, redelijk beknopt en kan gemakkelijk worden geparseerd door elke webtoepassing die JavaScript gebruikt. Go als moderne programmeertaal biedt eersteklas ondersteuning voor JSON-serialisatie in zijn standaardbibliotheek. 

Maar er zijn een paar hoekjes en gaatjes. In deze zelfstudie leer je hoe je willekeurige en gestructureerde gegevens effectief kunt serialiseren en deserialiseren naar / van JSON. Je leert ook hoe om te gaan met geavanceerde scenario's, zoals serialisatie-enums.

Het json-pakket

Go ondersteunt verschillende serialisatieformaten in het coderingspakket van zijn standaardbibliotheek. Een daarvan is het populaire JSON-formaat. U serialiseert Golang-waarden met de functie Marshal () in een segment bytes. U deserialiseert een segment bytes in een Golang-waarde met behulp van de Unmarshal () -functie. Het is zo simpel. De volgende termen zijn equivalent in de context van dit artikel:

  • Serialization / Encoding / Marshalling
  • Deserialisatie / decoderen / Unmarshalling

Ik geef de voorkeur aan serialisatie omdat dit het feit weerspiegelt dat je een potentieel hiërarchische gegevensstructuur converteert naar / van een stroom van bytes.

Maarschalk

De functie Marshal () kan alles aan, wat in Go de lege interface betekent en een segment bytes en fouten retourneert. Hier is de handtekening:

func Marshal (v-interface ) ([] byte, fout)

Als Marshal () er niet in slaagt de invoerwaarde te serialiseren, retourneert deze een niet-nulfout. Marshal () heeft een aantal strikte beperkingen (we zullen later zien hoe deze te verhelpen met custom marshallers):

  • Kaartsleutels moeten strings zijn.
  • Kaartwaarden moeten types serialiseerbaar zijn door het json-pakket.
  • De volgende typen worden niet ondersteund: kanaal, complex en functie.
  • Cyclische gegevensstructuren worden niet ondersteund.
  • Pointers worden gecodeerd (en later gedecodeerd) als de waarden waarnaar ze verwijzen (of 'null' als de aanwijzer nihil is).

unmarshal

De functie Unmarshal () neemt een byteplak die hopelijk geldige JSON en een bestemmingsinterface vertegenwoordigt, die meestal een verwijzing is naar een struct- of basistype. Het deserializes de JSON in de interface op een generieke manier. Als de serialisatie is mislukt, retourneert deze een fout. Hier is de handtekening:

func Unmarshal (data [] byte, v interface ) fout

Eenvoudige typen serialiseren

U kunt eenvoudig eenvoudige typen serialiseren, zoals het gebruik van het json-pakket. Het resultaat is geen volledig JSON-object, maar een eenvoudige reeks. Hier wordt de int 5 geserialiseerd naar de bytearray [53], wat overeenkomt met de string "5".

 // Serialize int var x = 5 bytes, err: = json.Marshal (x) if err! = Nil fmt.Println ("Can not serislize", x) fmt.Printf ("% v =>% v , '% v' \ n ", x, bytes, tekenreeks (bytes)) // Deserialize int var r int err = json.Unmarshal (bytes, & r) if err! = nil fmt.Println (" Can not deserislize ", bytes) fmt.Printf ("% v =>% v \ n ", bytes, r) Uitvoer: - 5 => [53], '5' - [53] => 5

Als u niet-ondersteunde typen zoals een functie probeert te serialiseren, krijgt u een foutmelding:

 // Proberen een functie te serialiseren foo: = func () fmt.Println ("foo () hier") bytes, err = json.Marshal (foo) if err! = Nil fmt.Println (err) Output : json: niet-ondersteund type: func ()

Seriële gegevens met behulp van kaarten serialiseren

De kracht van JSON is dat het zeer arbitraire hiërarchische gegevens kan representeren. Het JSON-pakket ondersteunt het en maakt gebruik van de generieke lege interface (interface ) om elke JSON-hiërarchie te vertegenwoordigen. Hier is een voorbeeld van het deserialiseren en later serialiseren van een binaire boom waarbij elk knooppunt een int-waarde heeft en twee takken links en rechts, die een ander knooppunt kunnen bevatten of null zijn.

De JSON null is gelijk aan de Go nil. Zoals je kunt zien in de uitvoer, de json.Unmarshal () functie heeft de JSON-blob succesvol geconverteerd naar een Go-gegevensstructuur bestaande uit een geneste kaart met interfaces en het waardetype als int behouden. De json.Marshal () functie heeft het resulterende genestelde object met succes geserialiseerd naar dezelfde JSON-representatie.

 // Arbitrary genest JSON dd: = '"value": 3, "left": "value": 1, "left": null, "right": "value": 2, "left": null, "right": null, "right": "value": 4, "left": null, "right": null 'var obj interface  err = json.Unmarshal ([] byte (dd) , & obj) if err! = nil fmt.Println (err) else fmt.Println ("-------- \ n", obj) data, err = json.Marshal (obj) if err ! = nil fmt.Println (err) else fmt.Println ("-------- \ n", string (data)) Uitvoer: -------- kaart [rechts : kaart [waarde: 4 links: rechts:] waarde: 3 links: map [links: rechts: kaart [waarde: 2 links: rechts:] waarde: 1]] -------- "left": "left": null, "right": "left": null, "right": null, "value": 2, "value": 1, "right": "left": null, "right": null, "value": 4, "value": 3 

Als u de generieke kaarten van interfaces wilt doorlopen, moet u typeaankondigingen gebruiken. Bijvoorbeeld:

func dump (obj interface ) fout if obj == nil fmt.Println ("nul") return nihil switch obj. (type) case bool: fmt.Println (obj. (bool)) case int: fmt.Println (obj. (int)) case float64: fmt.Println (obj. (float64)) case string: fmt.Println (obj. (string)) case map [string] interface : voor k, v: = bereik (obj. (kaart [tekenreeks] interface )) fmt.Printf ("% s:", k) err: = dump (v) als err! = nil return err standaard: fouten retourneren. Nieuw (fmt.Sprintf ("Unsupported type:% v", obj)) return nil

Serialiseren van gestructureerde gegevens

Werken met gestructureerde gegevens is vaak de betere keuze. Go biedt uitstekende ondersteuning voor het serialiseren van JSON van en naar structs via zijn struct -tags. Laten we een maken struct dat komt overeen met onze JSON-boom en een slimmer Dump () functie die het afdrukt:

type Tree struct waarde int left * Tree right * Tree func (t * Tree) Dump (inspringende tekenreeks) fmt.Println (inspringen + "waarde:", t.value) fmt.Print (inspringing + "left:" ) if t.left == nil fmt.Println (nil) else fmt.Println () t.left.Dump (indent + "") fmt.Print (indent + "right:") if t.right == nil fmt.Println (nil) else fmt.Println () t.right.Dump (indent + "") 

Dit is geweldig en veel schoner dan de willekeurige JSON-benadering. Maar werkt het? Niet echt. Er is geen fout, maar ons tree-object wordt niet gevuld door de JSON.

 jsonTree: = '"value": 3, "left": "value": 1, "left": null, "right": "value": 2, "left": null, "right": null , "right": "value": 4, "left": null, "right": null 'var tree Tree err = json.Unmarshal ([] byte (dd), & tree) if error! = nil fmt.Printf ("- Kan boom niet deserislize, fout:% v \ n", err) else tree.Dump ("") Uitvoer: waarde: 0 over:  rechts:  

Het probleem is dat de Tree-velden privé zijn. JSON-serialisatie werkt alleen op openbare velden. Dus we kunnen het maken struct velden openbaar. Het json-pakket is slim genoeg om de kleine letters "value", "left" en "right" op transparante wijze om te zetten in hun overeenkomstige hoofdletters.

type Tree struct Waarde int 'json: "waarde"' Links * Boom 'json: "left"' Rechts * Boom 'json: "right"' Uitgang: waarde: 3 links: waarde: 1 links:  rechts: waarde: 2 links:  rechts:  rechts: waarde: 4 links:  rechts:  

Het json-pakket negeert onopgemerkte velden in de JSON en privévelden in uw account struct. Maar soms wilt u misschien specifieke sleutels in de JSON toewijzen aan een veld met een andere naam in uw struct. Je kunt gebruiken struct tags daarvoor. Stel dat we een nieuw veld met de naam 'label' toevoegen aan de JSON, maar we moeten het toewijzen aan een veld met de naam 'Tag' in onze struct. 

type Tree struct Waarde int Labelstring 'json: "label"' Left * Tree Right * Tree func (t * Tree) Dump (inspringen-tekenreeks) fmt.Println (indent + "value:", t.Value) if t.Tag! = "" fmt.Println (indent + "tag:", t.Tag) fmt.Print (indent + "left:") if t.Left == nil fmt.Println (nil) else fmt.Println () t.Left.Dump (indent + "") fmt.Print (indent + "right:") if t.Right == nil fmt.Println (nil) else fmt.Println () t.Right.Dump (indent + "") 

Hier is de nieuwe JSON met het wortelknooppunt van de boom met het label "root", seriematig correct in het veld Tag en afgedrukt in de uitvoer:

 dd: = '"label": "root", "value": 3, "left": "value": 1, "left": null, "right": "value": 2, "left" : null, "right": null, "right": "value": 4, "left": null, "right": null 'var tree Tree err = json.Unmarshal ([] byte (dd ), & tree) if err! = nil fmt.Printf ("- boom kan niet deserislize, fout:% v \ n", err) else tree.Dump ("") Uitvoer: waarde: 3 tag: wortel links: waarde: 1 links:  rechts: waarde: 2 links:  rechts:  rechts: waarde: 4 links:  rechts: 

Een aangepaste Marshaller schrijven

U wilt vaak objecten serialiseren die niet voldoen aan de strenge eisen van de functie Marshal (). U wilt bijvoorbeeld een kaart met int-sleutels serialiseren. In deze gevallen kunt u een aangepaste marshaller / unmarshaller schrijven door het Marshaler en Unmarshaler interfaces.

Een opmerking over spelling: In Go is de conventie een interface met een enkele methode een naam te geven door het achtervoegsel "er" toe te voegen aan de naam van de methode. Dus hoewel de gebruikelijke spelling "Marshaller" is (met dubbele L), is de interfacenaam gewoon "Marshaler" (enkele L).

Dit zijn de interfaces tussen Marshall en Unmarshaler:

type Marshaler-interface MarshalJSON () ([] byte, fout) type Unmarshaler-interface UnmarshalJSON ([] byte) fout 

U moet een type maken wanneer u aangepaste serialisatie uitvoert, zelfs als u een ingebouwd type of samenstelling van ingebouwde typen wilt serialiseren, zoals kaart [int] string. Hier definieer ik een type genaamd IntStringMap en implementeer de Marshaler en Unmarshaler interfaces voor dit type.

De MarshalJSON () methode maakt een kaart [tekenreeks] string, converteert elk van de eigen int-sleutels naar een string en serialiseert de kaart met stringtoetsen volgens de standaard json.Marshal () functie.

type IntStringMap map [int] string func (m * IntStringMap) MarshalJSON () ([] byte, error) ss: = kaart [tekenreeks] tekenreeks  voor k, v: = bereik * m i: = strconv.Itoa (k) ss [i] = v return json.Marshal (ss) 

De UnmarshalJSON () -methode doet precies het tegenovergestelde. Het deserialiseert de databyte-array in een kaart [tekenreeks] string en converteert vervolgens elke tekenreekstoets naar een int en vult zichzelf in.

func (m * IntStringMap) UnmarshalJSON (gegevens [] byte) fout ss: = kaart [string] string  err: = json.Unmarshal (data, & ss) if err! = nil return err voor k, v: = bereik ss i, err: = strconv.Atoi (k) if err! = nil return err (* m) [i] = v return nil 

Hier is hoe het te gebruiken in een programma:

 m: = IntStringMap 4: "four", 5: "five" data, err: = m.MarshalJSON () if err! = nil fmt.Println (err) fmt.Println ("IntStringMap to JSON:" , string (data)) m = IntStringMap  jsonString: = [] byte ("\" 1 \ ": \" one \ ", \" 2 \ ": \" two \ "") m.UnmarshalJSON ( jsonString) fmt.Printf ("IntStringMap van JSON:% v \ n", m) fmt.Println ("m [1]:", m [1], "m [2]:", m [2]) Uitvoer : IntStringMap to JSON: "4": "four", "5": "five" IntStringMap van JSON: map [2: twee 1: één] m [1]: één m [2]: twee

Enums serialiseren

Ga enums kan behoorlijk lastig zijn om te serialiseren. Het idee om een ​​artikel over Go json-serialisatie te schrijven kwam voort uit een vraag die een collega me stelde over het rangschikken van enums. Hier is een Go enum. De constanten Zero en One zijn gelijk aan de ints 0 en 1.

type EnumType int const (Zero EnumType = iota One) 

Hoewel je misschien denkt dat het een int is, en dat is in veel opzichten, kun je het niet direct serialiseren. U moet een aangepaste marshaler / unmarshaler schrijven. Dat is geen probleem na het laatste gedeelte. Het volgende MarshalJSON () en UnmarshalJSON () serialiseert / deserialize de constanten NUL en EEN van / naar de bijbehorende tekenreeksen "Nul" en "Eén".

func (e * EnumType) UnmarshalJSON (data [] byte) error var s string err: = json.Unmarshal (data, & s) if err! = nil return err waarde, ok: = map [string] EnumType " Nul ": Nul," Eén ": Eén [s] als! Ok return errors.New (" Invalid EnumType value ") * e = value return nil func (e * EnumType) MarshalJSON () ([] byte , error) value, ok: = map [EnumType] string Zero: "Zero", One: "One" [* e] if! ok return nil, errors.New ("Invalid EnumType value") ga terug json.Marshal (waarde) 

Laten we proberen dit in te bedden EnumType in een struct en serialiseer het. De hoofdfunctie creëert een EnumContainer en initialiseert het met een naam van "Uno" en een waarde van onze enum constante EEN, wat gelijk is aan de int 1.

type EnumContainer struct Naam string Waarde EnumType func main () x: = Eén ec: = EnumContainer "Uno", x, s, err: = json.Marshal (ec) if err! = nil fmt.Printf ("niet!") var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", ec2.Value) Uitvoer: Uno: 0 

De verwachte uitvoer is "Uno: 1", maar in plaats daarvan is het "Uno: 0". Wat is er gebeurd? Er is geen fout in de marshal / unmarshal-code. Het blijkt dat u geen enums per waarde kunt insluiten als u ze wilt serialiseren. U moet een aanwijzer insluiten in het enum. Hier is een aangepaste versie waar dat werkt zoals verwacht:

type EnumContainer struct Naam string Waarde * EnumType func main () x: = Een ec: = EnumContainer "Uno", & x, s, err: = json.Marshal (ec) if err! = nil fmt. Printf ("niet!") Var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", * ec2.Value) Uitvoer: Uno: 1

Conclusie

Go biedt veel opties voor het serialiseren en deserialiseren van JSON. Het is belangrijk om de ins en outs van het coderings- / json-pakket te begrijpen om te profiteren van de kracht.

Deze tutorial heeft alle kracht in handen, inclusief hoe u de ongrijpbare Go-enums kunt serialiseren.

Ga een aantal objecten serialiseren!