Serializzazione JSON con Golang

Panoramica

JSON è uno dei formati di serializzazione più popolari. È leggibile dall'uomo, ragionevolmente conciso e può essere facilmente analizzato da qualsiasi applicazione web che utilizza JavaScript. Vai come un linguaggio di programmazione moderno ha il supporto di prima classe per la serializzazione JSON nella sua libreria standard. 

Ma ci sono alcuni angoli e fessure. In questo tutorial imparerai come serializzare in modo efficace e deserializzare dati arbitrari e strutturati da / verso JSON. Imparerai anche come gestire scenari avanzati come le enumerazioni di serializzazione.

Il pacchetto JSON

Go supporta diversi formati di serializzazione nel pacchetto di codifica della sua libreria standard. Uno di questi è il popolare formato JSON. Si serializzano i valori di Golang utilizzando la funzione Marshal () in una porzione di byte. Deserializzi una porzione di byte in un valore Golang utilizzando la funzione Unmarshal (). È così semplice. I seguenti termini sono equivalenti nel contesto di questo articolo:

  • Serializzazione / Codifica / Marshalling
  • Deserializzazione / decodifica / deserializzazione

Preferisco la serializzazione perché riflette il fatto che si converte una struttura di dati potenzialmente gerarchica in / da un flusso di byte.

Maresciallo

La funzione Marshal () può prendere qualsiasi cosa, che in Go significa l'interfaccia vuota e restituire una porzione di byte ed errori. Ecco la firma:

func Marshal (v interface ) ([] byte, errore)

Se Marshal () non riesce a serializzare il valore di input, restituirà un errore non nullo. Marshal () ha alcune limitazioni rigorose (vedremo in seguito come superarle con i marshaller personalizzati):

  • Le chiavi della mappa devono essere stringhe.
  • I valori della mappa devono essere tipi serializzabili dal pacchetto json.
  • I seguenti tipi non sono supportati: canale, complesso e funzione.
  • Le strutture dati cicliche non sono supportate.
  • I puntatori saranno codificati (e successivamente decodificati) come i valori a cui puntano (o 'nulli' se il puntatore è nullo).

unmarshal

La funzione Unmarshal () accetta una slice di byte che, auspicabilmente, rappresenta JSON valido e un'interfaccia di destinazione, che in genere è un puntatore a una struttura oa un tipo di base. Deserializza il JSON nell'interfaccia in modo generico. Se la serializzazione non è riuscita, verrà restituito un errore. Ecco la firma:

func Errore Unmarshal (data [] byte, v interfaccia )

Serializzazione di tipi semplici

Puoi serializzare facilmente tipi semplici come usare il pacchetto json. Il risultato non sarà un oggetto JSON completo, ma una stringa semplice. Qui int 5 è serializzato sull'array di byte [53], che corrisponde alla stringa "5".

 // Serializza int var x = 5 byte, err: = json.Marshal (x) se err! = Nil fmt.Println ("Can not serislize", x) fmt.Printf ("% v =>% v , '% v' \ n ", x, byte, stringa (byte)) // Deserializza int var r int err = json.Unmarshal (byte, & r) se err! = nil fmt.Println (" Can not deserislize ", byte) fmt.Printf ("% v =>% v \ n ", byte, r) Output: - 5 => [53], '5' - [53] => 5

Se provi a serializzare tipi non supportati come una funzione, riceverai un errore:

 // Cercando di serializzare una funzione foo: = func () fmt.Println ("foo () here") byte, err = json.Marshal (foo) se err! = Nil fmt.Println (err) Output : json: tipo non supportato: func ()

Serializzazione dei dati arbitrari con le mappe

La potenza di JSON è che può rappresentare molto bene i dati gerarchici arbitrari. Il pacchetto JSON lo supporta e utilizza l'interfaccia generica vuota (interface ) per rappresentare qualsiasi gerarchia JSON. Ecco un esempio di deserializzazione e serializzazione successiva di un albero binario in cui ogni nodo ha un valore int e due rami, sinistro e destro, che possono contenere un altro nodo o essere nulli.

Il JSON null è equivalente al Go nil. Come puoi vedere nell'output, il json.Unmarshal () la funzione ha convertito con successo il blob JSON in una struttura dati Go costituita da una mappa nidificata di interfacce e conservato il tipo di valore come int. Il json.Marshal () la funzione ha serializzato con successo l'oggetto nidificato risultante con la stessa rappresentazione JSON.

 // Arbitrary annidato 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) se err! = nil fmt.Println (err) else fmt.Println ("-------- \ n", obj) data, err = json.Marshal (obj) se err ! = nil fmt.Println (err) else fmt.Println ("-------- \ n", stringa (dati)) Output: -------- mappa [destra : map [valore: 4 a sinistra: destra:] valore: 3 a sinistra: mappa [a sinistra: a destra: mappa [valore: 2 a sinistra: destra:] valore: 1]] -------- "left": "left": null, "right": "left": null, "right": null, "value": 2, "value": 1, "right": "left": null, "right": null, "value": 4, "value": 3 

Per attraversare le mappe generiche di interfacce, è necessario utilizzare le asserzioni di tipo. Per esempio:

func dump (obj interface ) error if obj == nil fmt.Println ("nil") return nil commuta obj. (type) case bool: fmt.Println (obj. (bool)) case int: fmt.Println (obj (int)) case float64: fmt.Println (obj. (float64)) stringa del caso: fmt.Println (oggetto (stringa)) case map [stringa] interface : per k, v: = range (ogg (map [string] interface )) fmt.Printf ("% s:", k) err: = dump (v) se err! = nil return err default: restituisce errori. Nuovo (fmt.Sprintf ("Tipo non supportato:% v", obj)) return nil

Serializzazione dei dati strutturati

Lavorare con i dati strutturati è spesso la scelta migliore. Go fornisce un eccellente supporto per serializzare JSON da / a struct via suo struct tag. Creiamo a struct quello corrisponde al nostro albero JSON e un più intelligente Dump () funzione che la stampa:

digita Tree struct value int left * Albero a destra * Albero func (t * Albero) Dump (stringa indent) fmt.Println (indent + "valore:", t.value) 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 + "") 

Questo è fantastico e molto più pulito rispetto all'arbitrario approccio JSON. Ma funziona? Non proprio. Non ci sono errori, ma il nostro albero oggetto non viene popolato dal JSON.

 jsonTree: = '"value": 3, "left": "value": 1, "left": null, "right": "value": 2, "left": null, "right": null , "right": "value": 4, "left": null, "right": null 'albero var tree err = json.Unmarshal ([] byte (dd), & tree) se err! = nil fmt.Printf ("- Can not deserislize tree, error:% v \ n", err) else tree.Dump ("") Output: valore: 0 a sinistra:  destra:  

Il problema è che i campi Albero sono privati. La serializzazione JSON funziona solo su campi pubblici. Quindi possiamo fare il struct campi pubblici. Il pacchetto json è abbastanza intelligente da convertire in modo trasparente le chiavi in ​​minuscolo "value", "left" e "right" con i corrispondenti nomi di campi maiuscoli.

digita Tree struct Value int 'json: "value"' Left * Tree 'json: "left"' Right * Tree 'json: "right"' Output: valore: 3 left: valore: 1 left:  a destra: valore: 2 a sinistra:  destra:  a destra: valore: 4 a sinistra:  destra:  

Il pacchetto json ignorerà silenziosamente i campi non mappati nel JSON e i campi privati ​​nel tuo struct. Ma a volte potresti voler mappare chiavi specifiche nel JSON in un campo con un nome diverso nel tuo struct. Puoi usare struct tag per questo. Ad esempio, supponiamo di aggiungere un altro campo chiamato "etichetta" al JSON, ma dobbiamo mapparlo in un campo chiamato "Tag" nella nostra struttura. 

digita Tree struct Value int Tag string 'json: "label"' Left * Tree Right * Tree func (t * Tree) Dump (stringa indent) fmt.Println (indent + "value:", t.Value) se 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 + "") 

Ecco il nuovo JSON con il nodo radice dell'albero etichettato come "root", serializzato correttamente nel campo Tag e stampato nell'output:

 dd: = '"label": "root", "value": 3, "left": "value": 1, "left": null, "right": "value": 2, "left" : null, "right": null, "right": "value": 4, "left": null, "right": null 'albero var tree err = json.Unmarshal ([] byte (dd ), & tree) if err! = nil fmt.Printf ("- Can not deserislize tree, error:% v \ n", err) else tree.Dump ("") Output: valore: 3 tag: radice sinistra: valore: 1 a sinistra:  a destra: valore: 2 a sinistra:  destra:  a destra: valore: 4 a sinistra:  destra: 

Scrivere un Marshaller personalizzato

Spesso si desidera serializzare oggetti che non sono conformi ai severi requisiti della funzione Marshal (). Ad esempio, potresti voler serializzare una mappa con chiavi int. In questi casi, puoi scrivere un marshaller / unmarshaller personalizzato implementando il file Marshaler e Unmarshaler interfacce.

Una nota sull'ortografia: In Go, la convenzione è denominare un'interfaccia con un singolo metodo aggiungendo il suffisso "er" al nome del metodo. Quindi, anche se l'ortografia più comune è "Marshaller" (con doppia L), il nome dell'interfaccia è solo "Marshaler" (singola L).

Ecco le interfacce Marshaler e Unmarshaler:

digita l'interfaccia Marshaler MarshalJSON () ([] byte, errore) digita interfaccia Unmarshaler errore UnmarshalJSON ([] byte) 

È necessario creare un tipo quando si esegue la serializzazione personalizzata, anche se si desidera serializzare un tipo incorporato o una composizione di tipi predefiniti come Mappa [int] stringa. Qui definisco un tipo chiamato IntStringMap e implementare il Marshaler e Unmarshaler interfacce per questo tipo.

Il MarshalJSON () il metodo crea a Mappa [stringa] stringa, converte ciascuna delle proprie chiavi int in una stringa e serializza la mappa con chiavi stringa utilizzando lo standard json.Marshal () funzione.

digitare IntStringMap map [int] string func (m * IntStringMap) MarshalJSON () ([] byte, errore) ss: = map [stringa] stringa  per k, v: = intervallo * m i: = strconv.Itoa (k) ss [i] = v restituisce json.Marshal (ss) 

Il metodo UnmarshalJSON () fa l'esatto opposto. Deserializza l'array di byte di dati in a Mappa [stringa] stringa e quindi converte ogni chiave stringa in un int e si popola da solo.

func (m * IntStringMap) UnmarshalJSON (data [] byte) error ss: = map [string] string  err: = json.Unmarshal (data, & ss) se err! = nil return err per k, v: = range ss i, err: = strconv.Atoi (k) se err! = nil return err (* m) [i] = v return nil 

Ecco come usarlo in un programma:

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

Serializzazione Enums

Go enums può essere piuttosto irritante per serializzare. L'idea di scrivere un articolo sulla serializzazione di Go json è nata da una domanda che un collega mi ha chiesto su come serializzare le enumerazioni. Ecco un Go enum. Le costanti Zero e Uno sono uguali agli interi 0 e 1.

digitare EnumType int const (Zero EnumType = iota One) 

Sebbene tu possa pensare che sia un int, e per molti aspetti lo è, non puoi serializzarlo direttamente. Devi scrivere un marshaler / unmarshaler personalizzato. Questo non è un problema dopo l'ultima sezione. Il seguente MarshalJSON () e UnmarshalJSON () serializzerà / deserializzerà le costanti ZERO e ONE alle / dalle corrispondenti stringhe "Zero" e "Uno".

func (e * EnumType) UnmarshalJSON (data [] byte) error var s string err: = json.Unmarshal (data, & s) se err! = nil return err valore, ok: = map [string] EnumType " Zero ": Zero," Uno ": Uno [s] se! Ok return errors.New (" Valore EnumType non valido ") * e = valore restituito nil func (e * EnumType) MarshalJSON () ([] byte , errore) value, ok: = map [EnumType] string Zero: "Zero", Uno: "Uno" [* e] if! ok return nil, errors.New ("Valore EnumType non valido") return json.Marshal (valore) 

Proviamo a incorporare questo EnumType in un struct e serializzarlo. La funzione principale crea un EnumContainer e inizializza con un nome di "Uno" e un valore del nostro enum costante UNO, che è uguale a 1 int.

digita EnumContainer struct Nome stringa Valore EnumType func main () x: = Uno ec: = EnumContainer "Uno", x, s, err: = json.Marshal (ec) se err! = nil fmt.Printf ("fail!") var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", ec2.Value) Output: Uno: 0 

L'output atteso è "Uno: 1", ma è "Uno: 0". Quello che è successo? Non vi è alcun errore nel codice maresciallo / unmarsale. Si scopre che non è possibile incorporare enumerazioni di valore se si desidera serializzarle. È necessario incorporare un puntatore all'enumerazione. Ecco una versione modificata dove funziona come previsto:

digita EnumContainer struct Nome stringa Valore * EnumType func main () x: = Uno ec: = EnumContainer "Uno", & x, s, err: = json.Marshal (ec) se err! = nil fmt. Printf ("fail!") Var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", * ec2.Value) Output: Uno: 1

Conclusione

Go fornisce molte opzioni per serializzare e deserializzare JSON. È importante comprendere i dettagli del pacchetto di codifica / json per sfruttare la potenza.

Questo tutorial ti mette tutto il potere nelle tue mani, incluso come serializzare gli elusivi Go enum.

Vai serializzare alcuni oggetti!