JSON Serialization With Golang

Översikt

JSON är ett av de mest populära serialiseringsformaten. Det är mänskligt läsbart, rimligt koncist, och kan enkelt analyseras av alla webbapplikationer som använder JavaScript. Gå som ett modernt programmeringsspråk har förstklassigt stöd för JSON serialisering i sitt standardbibliotek. 

Men det finns några nooks och crannies. I den här handledningen lär du dig hur du serialiserar och deserialiserar godtyckliga såväl som strukturerad data till / från JSON. Du kommer också att lära dig hur du hanterar avancerade scenarier som serialisering enums.

Jsonpaketet

Go stöder flera serialiseringsformat i kodningspaketet i sitt standardbibliotek. En av dessa är det populära JSON-formatet. Du serialiserar Golang-värden med funktionen Marshal () i en bit av byte. Du deserialiserar en bit byte till ett Golang-värde med funktionen Unmarshal (). Det är så enkelt. Följande termer är likvärdiga i samband med denna artikel:

  • Serialisering / Kodning / Range
  • Deserialiseringsundantag / avkodning / Unmarshalling

Jag föredrar serialisering eftersom det speglar det faktum att du konverterar en potentiellt hierarkisk datastruktur till / från en ström av byte.

Marskalk

Funktionen Marshal () kan ta någonting, som i Go betyder det tomma gränssnittet och returnerar en bit bitgrupper och fel. Här är signaturen:

func Marshal (v gränssnitt ) ([] byte, fel)

Om Marshal () misslyckas med att serialisera ingångsvärdet, kommer det att returnera ett icke-nollfel. Marshal () har några stränga begränsningar (vi får se senare hur man kan övervinna dem med anpassade marshallare):

  • Kartnycklar måste vara strängar.
  • Kartvärden måste vara serialiserbara av json-paketet.
  • Följande typer stöds inte: Kanal, komplex och funktion.
  • Cykliska datastrukturer stöds inte.
  • Pekare kommer att kodas (och senare avkodas) som de värden de pekar på (eller "null" om pekaren är noll).

Unmarshal

Funktionen Unmarshal () tar en byte-skiva som förhoppningsvis representerar Giltigt JSON och ett destinationsgränssnitt, vilket typiskt är en pekare till en struktur eller en grundläggande typ. Det deserialiserar JSON in i gränssnittet på ett generiskt sätt. Om serialiseringen misslyckades kommer det att returnera ett fel. Här är signaturen:

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

Serialiserande enkla typer

Du kan enkelt serialisera enkla typer som att använda json-paketet. Resultatet kommer inte att vara ett fullfjädrad JSON-objekt, utan en enkel sträng. Här serieras int 5-seriet till byte-arrayen [53], vilket motsvarar strängen "5".

 // Serialize int var x = 5 byte, err: = json.Marshal (x) om err! = Nil fmt.Println ("Kan inte serislize", x) fmt.Printf ("% v =>% v , '% v' \ n ", x, byte, sträng (byte)) // Deserialize int var r int err = json.Unmarshal (bytes, & r) om err! = nil fmt.Println (" kan inte deserislize ", bytes) fmt.Printf ("% v =>% v \ n ", byte, r) Utgång: - 5 => [53], '5' - [53] => 5

Om du försöker serialisera ostödda typer som en funktion får du ett fel:

 // Försök att serialisera en funktion foo: = func () fmt.Println ("foo () här") byte, err = json.Marshal (foo) om err! = Nil fmt.Println (err) : json: ostödd typ: func ()

Serialiserar godtyckliga data med kartor

Kraften hos JSON är att den kan representera godtyckliga hierarkiska data mycket bra. JSON-paketet stöder det och använder det generiska tomma gränssnittet (gränssnitt ) för att representera någon JSON-hierarki. Här är ett exempel på deserialisering och senare serialisering av ett binärt träd där varje nod har ett intvärde och två grenar, vänster och höger, vilket kan innehålla en annan nod eller vara null.

JSON null motsvarar Go nil. Som du kan se i utgången, json.Unmarshal () funktionen lyckades konvertera JSON blob till en Go-datastruktur bestående av en kapslade gränssnittskort och bevara värdetypen som int. De json.Marshal () funktionen serialiserat framgångsrikt det resulterande nestade objektet till samma JSON-representation.

 // Vilkårlig nestad JSON dd: = '"värde": 3, "vänster": "värde": 1, "vänster": null, "rätt": "värde": 2, "vänster" "right": null, "right": "värde": 4, "vänster": null, "rätt": null "var obj interface  err = json.Unmarshal ([] byte (dd) , & obj) om err! = nil fmt.Println (err) annars fmt.Println ("-------- \ n", obj) data, err = json.Marshal (obj) om fel ! = nil fmt.Println (err) else fmt.Println ("-------- \ n", sträng (data) Utgång: -------- karta [höger : karta [värde: 4 vänster: höger:] värde: 3 kvar: karta [vänster: höger: karta [värde: 2 kvar: höger:] värde: 1]] -------- "left": "left": null, "right": "left": null, "right": null, "värde": 2 "värde": 1, "rätt": "vänster": null, "rätt": null, "värde": 4, "värde": 3 

För att korsa de generiska kartorna över gränssnitt måste du använda typkrav. Till exempel:

funk dump (obj interface ) fel om obj == nil fmt.Println ("nil") returnera nil switch obj. (typ) fall bool: fmt.Println (obj. fmt.Println (obj. (int)) fall float64: fmt.Println (obj. (float64)) fallsträng: fmt.Println (obj. (sträng)) fallkarta [string] gränssnitt : för k, v: = range (obj. (map [string] gränssnitt )) fmt.Printf ("% s:", k) err: = dumpa (v) om err! = nil return err standard: returfel. Ny (fmt.Sprintf ("Ostödd typ:% v", obj)) returnera nil

Serialiserande strukturerade data

Att arbeta med strukturerad data är ofta det bättre valet. Go ger utmärkt stöd för serialisering av JSON till / från structs via dess struct taggar. Låt oss skapa en struct Det motsvarar vårt JSON-träd och en smartare Dumpa() funktion som skriver ut det:

typ Tree struktur värde int vänster * Tree right * Tree func (t * Tree) Dump (strecksträng) fmt.Println (indent + "värde:", t.value) fmt.Print (indent + "left:" ) om t.left == nil fmt.Println (nil) annars fmt.Println () t.left.Dump (indent + "") fmt.Print (indent + "right:") om t.right == nil fmt.Println (nil) annars fmt.Println () t.right.Dump (indent + "") 

Detta är bra och mycket renare än den godtyckliga JSON-metoden. Men fungerar det? Inte riktigt. Det finns inget fel, men vårt trädobjekt blir inte befolket av JSON.

 jsonTree: = '"värde": 3, "vänster": "värde": 1, "vänster": null, "rätt": "värde": 2, "vänster": null, "höger": null , "right": "värde": 4, "vänster": null, "rätt": null 'var träd Tree err = json.Unmarshal ([] byte (dd) nil fmt.Printf ("- kan inte deserislize tree, error:% v \ n", err) else tree.Dump ("") Utgång: värde: 0 kvar:  höger:  

Problemet är att trädfälten är privata. JSON serialisering fungerar endast på offentliga områden. Så vi kan göra struct fält offentliga. Json-paketet är smart nog att omvandla de små bokstäverna "värde", "vänster" och "höger" till deras motsvarande stora versionsfält.

Vänster * Tree "json:" left "" Höger * Tree "json:" right "' Utgång: värde: 3 kvar: värde: 1 kvar:  höger: värde: 2 kvar:  höger:  höger: värde: 4 kvar:  höger:  

Json-paketet kommer tyst att ignorera omappnade fält i JSON såväl som privata fält i din struct. Men ibland kanske du vill kartera specifika nycklar i JSON till ett fält med ett annat namn i din struct. Du kan använda struct taggar för det. Anta att vi till exempel lägger till ett annat fält som heter "label" till JSON, men vi måste kartlägga det till ett fält som heter "Tag" i vår struct. 

typ Tree-struktur Value int Tag-sträng 'json:' label '' Vänster * Tree Right * Tree func (t * Tree) Dump (indentsträng) fmt.Println (indent + "värde:", t.Value) om t.Tag! = "" fmt.Println (indent + "tagg:", t.Tag) fmt.Print (indent + "left:") om t.Left == nil fmt.Println (nil) annars fmt.Println () t.Left.Dump (indent + "") fmt.Print (indent + "right:") om t.Right == nil fmt.Println (nil) else fmt.Println () t.Right.Dump (indent + "") 

Här är den nya JSON med trädets rotnod som är märkt som "root", serialiseras korrekt i Tag-fältet och skrivs ut i utmatningen:

 dd: = '"etikett": "root", "value": 3, "left": "värde": 1, "vänster": null, "rätt": "värde": 2, "vänster" : null, "rätt": null, "rätt": "värde": 4, "vänster": null, "rätt": null 'var träd Tree err = json.Unmarshal ([] byte ), och tree) om err! = nil fmt.Printf ("- kan inte deserisliera träd, fel:% v \ n", err) annat tree.Dump ("") Output: value: 3 tag: root left: värde: 1 kvar:  höger: värde: 2 kvar:  höger:  höger: värde: 4 kvar:  höger: 

Skriva en anpassad Marshaller

Du vill ofta serialisera objekt som inte överensstämmer med de strikta kraven i Marshal () -funktionen. Till exempel kanske du serialiserar en karta med int nycklar. I dessa fall kan du skriva en anpassad marshaller / unmarshaller genom att implementera Marshaler och Unmarshaler gränssnitt.

En anteckning om stavning: I Go är konventionen att namnet ett gränssnitt med en enda metod genom att lägga till "er" suffixet till metodnamnet. Så, även om den mer vanliga stavningen är "Marshaller" (med dubbla L), är gränssnittsnamnet bara "Marshaler" (singel L).

Här är Marshaler och Unmarshaler gränssnitt:

skriv Marshaler-gränssnittet MarshalJSON () ([] byte, fel) skriv Unmarshaler-gränssnittet UnmarshalJSON ([] byte) error 

Du måste skapa en typ när du gör anpassad serialisering, även om du vill serialisera en inbyggd typ eller komposition av inbyggda typer som karta [int] string. Här definierar jag en typ som heter IntStringMap och implementera Marshaler och Unmarshaler gränssnitt för denna typ.

De MarshalJSON () Metoden skapar en karta [sträng] sträng, konverterar var och en av sina egna int nycklar till en sträng och serialiserar kartan med strängnycklar med hjälp av standarden json.Marshal () fungera.

skriv intStringMap-karta [int] strängfunktion (m * IntStringMap) MarshalJSON () ([] byte, fel) ss: = map [string] string  för k, v: = range * m i: = strconv.Itoa (k) ss [i] = v returnera json.Marshal (ss) 

Metoden UnmarshalJSON () gör exakt motsatsen. Det deserialiserar data byte array till a karta [sträng] sträng och konverterar sedan varje strängnyckel till en int och fyller sig själv.

func (m * IntStringMap) UnmarshalJSON (data [] byte) fel ss: = map [string] string  err: = json.Unmarshal (data, & ss) om err! = nil return err för k, v: = intervall ss i, err: = strconv.Atoi (k) om err! = nil return err (* m) [i] = v returnera nil 

Så här använder du det i ett program:

 m: = IntStringMap 4: "four", 5: "five" data, err: = m.MarshalJSON () om err! = nil fmt.Println (err) fmt.Println ("IntStringMap till JSON:" , sträng (data)) m = IntStringMap  jsonString: = [] byte ("\" 1 \ ": \" one \ ", \" 2 \ ": \" two \ "" m.UnmarshalJSON jsonString) fmt.Printf ("IntStringMap från JSON:% v \ n", m) fmt.Println ("m [1]:", m [1], "m [2]:", m [2]) : IntStringMap till JSON: "4": "four", "5": "five" IntStringMap från JSON: karta [2: två 1: en] m [1]: en m [2]: två

Serialiserande Enums

Go enums kan vara ganska växande att serialize. Tanken att skriva en artikel om Go json serialisering kom ut ur en fråga som en kollega frågade mig om hur man serialiserar enums. Här är en Go enum. Konstanterna Zero och One är lika med intsna 0 och 1.

skriv EnumType int const (Zero EnumType = iota One) 

Medan du kanske tror att det är ett int, och i många avseenden är det, kan du inte serialisera det direkt. Du måste skriva en anpassad marshaler / unmarshaler. Det är inte ett problem efter det sista avsnittet. Det följande MarshalJSON () och UnmarshalJSON () kommer serialisera / deserialisera konstanterna ZERO och ONE till / från motsvarande strängar "Zero" och "One".

func (e * EnumType) UnmarshalJSON (data [] byte) fel var s sträng err: = json.Unmarshal (data, & s) om err! = nil return err värde, ok: = map [string] EnumType  Noll ": Noll," En ": En [s] om! Ok returfel.Ny (" Ogiltigt EnumType-värde ") * e = Värdeåtergång nil func (e * EnumType) MarshalJSON () , fel) värde, ok: = karta [EnumType] sträng Zero: "Zero", One: "One" [* e] om! ok return null, errors.New ("Invalid EnumType value") returnera json.Marshal (värde) 

Låt oss försöka bädda in det här EnumType i en struct och serialisera det. Huvudfunktionen skapar en EnumContainer och initialiserar det med namnet "Uno" och ett värde av vårt enum konstant ETT, vilket är lika med int 1.

skriv EnumContainer struct Namnsträng Värde EnumType func main () x: = En ec: = EnumContainer "Uno", x, s, err: = json.Marshal (ec) om err! = nil fmt.Printf ("fail") var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", ec2.Value) Utgång: Uno: 0 

Den förväntade utgången är "Uno: 1", men i stället är det "Uno: 0". Vad hände? Det finns ingen bugg i marshal / oförändrad kod. Det visar sig att du inte kan inbädda enums med värde om du vill serialisera dem. Du måste bädda in en pekare till enummen. Här är en modifierad version där det fungerar som förväntat:

skriv EnumContainer struct Namnsträng Värde * EnumType func main () x: = En ec: = EnumContainer "Uno", & x, s, err: = json.Marshal (ec) om err! = nil fmt. Printf ("fail!") Var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", * ec2.Value) Utgång: Uno: 1

Slutsats

Go ger många alternativ för serialisering och deserialisering av JSON. Det är viktigt att förstå insatserna för kodning / json-paketet för att dra nytta av kraften.

Denna handledning lägger all kraft i dina händer, inklusive hur man serialiserar de elusive Go enums.

Gå serialisera vissa objekt!