Die Golumne

JavaScript in der Welt von Go: Wie, JavaScript?

Frank Müller

© Reneé French (CC BY 3.0 DE) ©SuS Media

Naja, es geht nicht direkt um JavaScript, keine Panik. Doch die JavaScript Object Notation, kurz JSON, ist derzeit die Lingua Franca für den Datenaustausch im Internet. Eine Vielzahl von Web APIs, seien sie nun RESTful oder nicht, nutzen dieses Format für den Austausch von Daten. Andere Programme wiederum speichern ihre Daten sowohl als Textdateien oder in entsprechenden Datenbanken wie CouchDB in der JSON. Und auch für Konfigurationen wird JSON gerne genutzt.

Über Sinn und Unsinn dieses Formats mag ich hier nicht sinnieren. Es hat seine Vorteile, kann in unterschiedlichen Anwendungen jedoch auch durch individuell bessere Alternativen ersetzt werden. Wie dem auch sei – auch für Go-Entwickler ist JSON ein Thema. Und so verwundert es nicht, dass das Package encoding/json seit Anbeginn Bestandteil der Standardbibliothek ist. Hierbei fällt sehr angenehm auf, wie leicht dieses Paket zu nutzen ist. Als Beispiel sei an der Stelle folgendes Adressformat definiert.

 type Address struct {
  Street string
  City   string
}

Was auffällt, ist die Notation der Felder in Versalien, sie werden also exportiert. Das ist notwendig, damit das mittels Reflection arbeitende Encoding die Felder auch erkennen kann. Insofern rate ich, für den Transfer von und zu JSON eigene Typen jenseits möglicher Typen mit eigenen Methoden zu verwenden. Das Marshalling selbst geht nun sehr einfach.

chancellery := Address{
  Street: "Willi-Brandt-Straße 1",
  City:   "10557 Berlin",
}

b, err := json.Marshal(chancellery)

Nun ist b vom Typ []byte und enthält das JSON ohne Umbrüche und Einrückungen.

{"Street": "Willi-Brand-Straße 1", "City": "10557 Berlin"}

Für einen Output mit Zeilenumbruch und Einrückungen stellt das Package die Funktion MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) zur Verfügung. So leicht wie die Wandlung der Go-Daten nach JSON ist auch Umwandlung eines JSON-Dokuments zurück nach Go.

var a Address

err := json.Unmarshal(b, &a). 

Hier wird zuerst die entsprechende Variable erzeugt. Sie kann eine einfache Basisvariable oder (so wie hier) auch eine Struktur sein. Nur muss sie zu den Daten passen, denn sie stellt den Platz für die Unmarshal()-Funktion zur Verfügung und dient via Reflection als Muster.

DevOpsCon Istio Cheat Sheet

Free: BRAND NEW DevOps Istio Cheat Sheet

Ever felt like service mesh chaos is taking over? Then our brand new Istio cheat sheet is the right one for you! DevOpsCon speaker Michael Hofmann has summarized Istio’s most important commands and functions. Download FOR FREE now & sort out your microservices architecture!

Nun könnten die zum Marshalling gehörenden Byte-Slices sehr groß werden, je nach Komplexität der Typen oder Umfang der Daten. Ein Beispiel sei eine Abfrage einer größeren Menge an Daten aus einer Datenbank, um diese an einen anderen Service oder ein Web-Frontend zu übertragen. Dafür stellt das Package entsprechende Typen für das Encoding direkt in einen io.Writer beziehungsweise das Decoding aus einem io.Reader zur Verfügung. So werden kleinere Blöcke gestreamt, anstatt umfangreich den Speicher zu belegen.

Sonderzeichen

Damit klappt doch alles, also können wir das Thema eigentlich abhaken. Doch halt, nein, ich muss nun folgendes Dokument verarbeiten.

{
  "device": "/dev/eth0",
  "min-throughput": 10,
  "max-throughput": 921,
  "avg-throughput": 401
}

Das ist aus der Sicht von JSON valide. Also muss ich flugs eine passende Struktur aufsetzen, das kann ja so schwer nicht sein:

type Throughput struct {
  device         string
  min-throughput int
  max-throughput int
  avg-throughput int
}

Doch halt: Das soll gehen? Nein, natürlich nicht, da das Minuszeichen nicht im Variablennamen vorkommen darf. Und wenn ich es entferne? Auch dann funktioniert es nicht, da das Marshalling, wie bereits erwähnt, auf Reflection basiert und hierzu die Felder ja exportiert werden müssen. Ich muss sie also großschreiben. Doch das hat wiederum Einfluss auf die Schreibweise der Felder auf JSON. Irgendwie passt das alles nicht, wir drehen uns im Kreis.

Glücklicherweise verfügt Go für Situationen wie diese über die sogenannten Field-Tags. Mit ihnen können über Schlüssel-Wert-Paare verschiedene Optionen für ein Feld gesetzt und via Reflection ausgelesen werden. Diese Fähigkeit ist nicht auf das Package encoding/json beschränkt, dieses nutzt jedoch diese Fähigkeit. Es setzt dabei – naheliegend – auf den Key json. Über dessen Werte lassen sich die Namen verändern, das Auslassen leerer Felder beim Marshalling steuern, Felder bei Bedarf generell ignorieren oder auch die Nutzung eines abweichenden Typs zwischen JSON und GO beeinflussen. Im Beispiel oben könnte das wie in Listing 1 aussehen.

type Throughput struct {
  Device   string  json:"device"
  Min      int     json:"min-throughput,omitempty"
  Max      int     json:"max-throughput,omitempty"
  Avg      int     json:"avg-throughput,omitempty"
  Internal string  json:"-"
}

Hier ist nun Device ein Feld, das immer gesetzt wird. Bei Min, Max und Avg ist der Name des Felds im JSON-Dokument deutlich zu erkennen. Haben diese den Wert 0, werden sie dank omitempty nicht serialisiert. Und das neue Feld Internal wird bei einer Serialisierung ignoriert. Allerdings ist der Bindestrich bei JSON ja auch ein gültiger Feldname, was nun? Hier nutzt das Package den Field-Tag json:“-,“, er führt zum gewünschten Ergebnis.

In Go gibt es Field-Tags, mit denen über Schlüssel-Wert-Paare Optionen für ein Feld gesetzt und via Reflection ausgelesen werden können.

Typfrage

Ein weiterer Wunsch bleibt der Umgang mit Typen. Ein Beispiel ist folgendes Szenario: Der Wert auf Seiten von Go zwecks Weiterverarbeitung ist ein int, eine externe Schnittstelle erwartet den Wert aber als string. Das ist mit json:“name,string“ möglich, denn das Marshalling stellt die Zahl als String dar. Beim Unmarshal() kann es natürlich passieren, dass die Zeichenkette im JSON auch einen Buchstaben oder ein anderes ungültiges Zeichen enthält. Hier bricht der Befehl mit einer entsprechenden Fehlermeldung ab.

Nun kann ich aber auch Typen haben, die ich gerne in einem ganz eigenen Format serialisieren möchte, etwa zur Darstellung von Integern als römische Zahlen. Hierfür bietet das Paket die beiden Interfaces Marshaler und Unmarshaler. Sie definieren die entsprechenden Methoden MarshalJSON() ([]byte, error) und UnmarshalJSON(data []byte) error. Mit der Implementierung dieser Methoden für den eigenen Typ RomanNumber wird die Verantwortung für Serialisierung und Deserialisierung an diesen Typ übergeben.

Ein weiterer praktischer Helfer ist der Typ RawMessage. Die Basis ist nur eine Byte-Slice, doch sie implementiert mit ihren Methoden die Interfaces Marshaler und Unmarshaler. So lässt sich die Deserialisierung von Feldern verzögern oder ihre Serialisierung vorverarbeiten. Auch die Arbeit mit flexiblen Dokumenten ist auf diesem Weg möglich (Listing 2).

type Drink struct {
  Type string
  Data json.RawMessage
}

type Juice struct {
  Fruit        string
  FruitContent float64
}

type Whisky struct {
  Region  string
  Casks   string
  Alcohol float64
}

var d Drink
err := json.Unmarshal(doc, &d)

switch d.Type {
case "Juice":
  var j Juice
  err = json.Unmarshal(d.Data, &j)
case "Whisky":
  var w Whisky
  err = json.Unmarshal(d.Data, &w)
}

Die entsprechenden Dokumente können also im Data-Feld über unterschiedliche Unterstrukturen verfügen, in Listing 2 also nur über Saft und Whisky. Doch theoretisch kann es beliebig flexibel gehalten werden.

Aber ich kenne es doch nicht!

Bisher setzte das Deserialisieren von Dokumenten auf der Bekanntheit der Strukturen auf, doch das ist nicht immer der Fall. Ab und an sind die zu lesenden Dokumente unbekannt. Hiermit kann Go ebenfalls umgehen, wenn auch etwas unkomfortabel. Als Ziel wird in diesem Fall die Referenz auf eine Variable vom Typ interface{} gegeben. Die entsprechend konkreten Typen leitet das Package dann aus den JSON-Daten selbst ab. Sie können hinterher via Type-Assertion bestimmt werden (Listing 3).

var d interface{}
err := json.Unmarshal(doc, &d)

switch v := d.(type) {
case string:
  fmt.Printf("%v ist eine Zeichenkette", v)
case int:
  fmt.Printf("%v ist ein Integer", v)
case float64:
  fmt.Printf("%v ist eine Fließkommazahl", v)
}

Doch wie ist es nun, wenn es sich bei dem Dokument um eine Struktur oder eine Liste handelt? Oder wenn die Werte von Feldern in diesem Dokument wiederum selbst Strukturen oder Listen sind? Sie werden in entsprechenden generischen Typen abgebildet. Eine Struktur ist hierbei eine Map vom Typ map[string]interface{}, eine Liste ist eine passende Slice vom Typ []interface{}. Für die jeweiligen Inhalte gelten dann die gleichen Regeln wie zuvor. Die Umwandlung in den konkreten Typ hat also wieder über eine Type-Assertion zu erfolgen. Das ist wenig komfortabel und zudem noch unsicher. Hier helfen Packages wie der Tideland Generic JSON Processor oder Gabs bei der leichteren Navigation, Suche und Verarbeitung.

Aber so lahm?

Bleibt noch ein kleines Problem: Wie bereits oben erwähnt, nutzt das Package encode/json die Reflection zur Analyse der zu serialisierenden Daten oder der übergebenen Struktur für eine Deserialisierung. Diese ist zwar flexibel, doch auch nicht gerade für ihre Geschwindigkeit bekannt. Daher bietet die Open-Source-Szene performantere Alternativen an, etwa fastjson , Json Parser oder Jsoniter. Neben ihrer höheren Geschwindigkeit bringen diese in der Regel auch ein umfangreicheres API zur Navigation in den Dokumenten oder zur Erzeugung dieser mit. Jsoniter bietet zudem eine kompatible Variante für die Aufrufe von json.Marshal() und json.Unmarshal(). Daher sollten diese Bibliotheken bei komplexeren Aufgaben oder größeren Datenmengen evaluiert werden.

Fazit

Wie schon gesagt, an JavaScripts nativem Format JSON kommt man heute aus vielerlei Gründen und in unterschiedlichsten Anwendungen nicht vorbei. Es ist nicht immer die optimale Notation, aber ein guter Kompromiss aus Flexibilität und Lesbarkeit bei geringerem Overhead als beispielsweise XML.

Gerade für den recht gängigen Einsatz zum Datenaustausch zwischen Microservices oder Web-Frontends via RESTful API stellt sich das Package encoding/json sehr gut auf. Hier geht es vielfach darum, Entitäten oder abgeleitete Typen serialisiert über HTTP zu übertragen. Und wenn sich ein Go Service an die Notation der anderen Services anpassen muss, ist das über die Tags kein Problem. Vielleicht ist diese Flexibilität einer der Gründe, warum Go derzeit seinen Einsatz im Feld der Webanwendungen ausweitet.

Geschrieben von
Frank Müller
Frank Müller
Der Oldenburger Frank Müller ist seit über dreißig Jahren in der IT zu Hause und im Netz vielfach als @themue anzutreffen. Das Interesse an Googles Sprache Go begann 2009 und führte inzwischen zu einem Buch sowie mehreren Artikeln und Vorträgen zum Thema.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: