Wie man Struktur-Tags in Go verwendet

Einführung

Strukturen oder structs werden verwendet, um mehrere Informationen in einer Einheit zusammenzufassen. Diese Informationskollektionen werden verwendet, um Konzepte auf höherer Ebene zu beschreiben, wie z.B. eine Adresse, die aus einer Straße, einer Stadt, einem Bundesstaat und einem Postleitzahl besteht. Wenn Sie diese Informationen aus Systemen wie Datenbanken oder APIs lesen, können Sie Struktur-Tags verwenden, um zu steuern, wie diese Informationen den Feldern einer Struktur zugewiesen werden. Struktur-Tags sind kleine Stücke von Metadaten, die an Felder einer Struktur angehängt sind und Anweisungen für anderen Go-Code bereitstellen, der mit der Struktur arbeitet.

Wie sieht ein Struktur-Tag aus?

Go-Struktur-Tags sind Annotationen, die nach dem Typ in einer Go-Strukturdeklaration erscheinen. Jedes Tag besteht aus kurzen Zeichenfolgen, die mit einem entsprechenden Wert verknüpft sind.

A struct tag looks like this, with the tag offset with backtick ` characters:

type User struct {
	Name string `example:"name"`
}

Andere Go-Code ist dann in der Lage, diese Strukturen zu untersuchen und die Werte abzurufen, die spezifischen angeforderten Schlüsseln zugewiesen sind. Struktur-Tags haben keine Auswirkungen auf die Funktion Ihres Codes ohne zusätzlichen Code, der sie untersucht.

Versuchen Sie dieses Beispiel, um zu sehen, wie Strukturtags aussehen, und dass sie ohne Code aus einem anderen Paket keine Wirkung haben werden.

package main

import "fmt"

type User struct {
	Name string `example:"name"`
}

func (u *User) String() string {
	return fmt.Sprintf("Hi! My name is %s", u.Name)
}

func main() {
	u := &User{
		Name: "Sammy",
	}

	fmt.Println(u)
}

Dies wird ausgegeben:

Output
Hi! My name is Sammy

Dieses Beispiel definiert einen User-Typ mit einem Name-Feld. Dem Name-Feld wurde ein Strukturtag von example:"name" zugewiesen. Wir würden in einem Gespräch auf diesen spezifischen Tag als „Beispiel-Strukturtag“ verweisen, weil er das Wort „Beispiel“ als Schlüssel verwendet. Der example-Strukturtag hat den Wert "name" für das Name-Feld. Auf dem User-Typ definieren wir auch die String()-Methode, die vom fmt.Stringer-Interface benötigt wird. Diese wird automatisch aufgerufen, wenn wir den Typ an fmt.Println übergeben, und gibt uns die Möglichkeit, eine schön formatierte Version unserer Struktur zu erzeugen.

Innerhalb des Körpers von main erstellen wir eine neue Instanz unseres User-Typs und übergeben ihn an fmt.Println. Auch wenn die Struktur einen Strukturtag hatte, sehen wir, dass er keine Auswirkung auf die Funktion dieses Go-Codes hat. Es wird sich genau gleich verhalten, wenn der Strukturtag nicht vorhanden wäre.

Um Strukturtag zu verwenden, um etwas zu erreichen, muss anderer Go-Code geschrieben werden, um Strukturen zur Laufzeit zu untersuchen. Die Standardbibliothek enthält Pakete, die Strukturtag als Teil ihrer Operation verwenden. Das beliebteste davon ist das encoding/json-Paket.

JSON-Codierung

JavaScript Object Notation (JSON) ist ein textuelles Format zur Codierung von Datensammlungen, die unter verschiedenen Zeichenfolgenschlüsseln organisiert sind. Es wird häufig verwendet, um Daten zwischen verschiedenen Programmen zu kommunizieren, da das Format einfach genug ist und Bibliotheken existieren, um es in vielen verschiedenen Sprachen zu decodieren. Das Folgende ist ein Beispiel für JSON:

{
  "language": "Go",
  "mascot": "Gopher"
}

Dieses JSON-Objekt enthält zwei Schlüssel, language und mascot. Nach diesen Schlüsseln folgen die zugehörigen Werte. Hier hat der Schlüssel language den Wert Go und mascot ist mit dem Wert Gopher belegt.

Der JSON-Encoder in der Standardbibliothek verwendet Strukturtags als Annotationen, die dem Encoder anzeigen, wie Sie Ihre Felder im JSON-Ausgabegerät benennen möchten. Diese JSON-Codierungs- und Decodierungsmechanismen finden sich im encoding/json Paket.

Versuchen Sie dieses Beispiel, um zu sehen, wie JSON ohne Strukturtags codiert wird:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string
	Password      string
	PreferredFish []string
	CreatedAt     time.Time
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Dies wird die folgende Ausgabe drucken:

Output
{ "Name": "Sammy the Shark", "Password": "fisharegreat", "CreatedAt": "2019-09-23T15:50:01.203059-04:00" }

Wir haben eine Struktur definiert, die einen Benutzer beschreibt, mit Feldern einschließlich ihres Namens, Passworts und der Zeit, zu der der Benutzer erstellt wurde. Innerhalb der main-Funktion erstellen wir eine Instanz dieses Benutzers, indem wir Werte für alle Felder außer PreferredFish angeben (Sammy mag alle Fische). Anschließend haben wir die Instanz von User an die Funktion json.MarshalIndent übergeben. Dies wird verwendet, damit wir die JSON-Ausgabe einfacher sehen können, ohne ein externes Formatierungstool zu verwenden. Dieser Aufruf könnte durch json.Marshal(u) ersetzt werden, um JSON ohne zusätzliche Leerzeichen zu drucken. Die beiden zusätzlichen Argumente für json.MarshalIndent steuern das Präfix für die Ausgabe (das wir mit dem leeren String weggelassen haben) und die Zeichen, die für die Einrückung verwendet werden sollen, die hier zwei Leerzeichen sind. Alle Fehler, die von json.MarshalIndent erzeugt werden, werden protokolliert, und das Programm wird mit os.Exit(1) beendet. Schließlich haben wir das von json.MarshalIndent zurückgegebene []byte in eine string umgewandelt und die resultierende Zeichenkette an fmt.Println übergeben, um sie auf dem Terminal zu drucken.

Die Felder der Struktur erscheinen genau so, wie sie benannt sind. Dies entspricht jedoch nicht dem typischen JSON-Stil, den Sie erwarten würden, der für die Feldnamen Kamelkasten verwendet. Sie werden die Namen der Felder in diesem nächsten Beispiel in Kamelkasten-Stil ändern. Wie Sie sehen werden, wenn Sie dieses Beispiel ausführen, wird dies nicht funktionieren, weil die gewünschten Feldnamen mit den Regeln von Go für exportierte Feldnamen in Konflikt stehen.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	name          string
	password      string
	preferredFish []string
	createdAt     time.Time
}

func main() {
	u := &User{
		name:      "Sammy the Shark",
		password:  "fisharegreat",
		createdAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Dies ergibt die folgende Ausgabe:

Output
{}

In dieser Version haben wir die Namen der Felder geändert, um sie in Camel Case zu benennen. Jetzt ist Name name, Password ist password, und schließlich CreatedAt ist createdAt. Im Körper von main haben wir die Instanziierung unseres Strukturs geändert, um diese neuen Namen zu verwenden. Wir übergeben dann den Struktur an die Funktion json.MarshalIndent wie zuvor. Die Ausgabe ist dieses Mal ein leeres JSON-Objekt, {}.

Das korrekte Benennen von Feldern in Camel Case erfordert, dass der erste Buchstabe kleingeschrieben wird. Während JSON nicht darauf achtet, wie Sie Ihre Felder benennen, tut Go dies, da es die Sichtbarkeit des Feldes außerhalb des Pakets angibt. Da das Paket encoding/json ein separates Paket vom Paket main ist, das wir verwenden, müssen wir den ersten Buchstaben großschreiben, um ihn für encoding/json sichtbar zu machen. Es scheint, dass wir an einem Dilemma stehen. Wir brauchen eine Möglichkeit, dem JSON-Encoder mitzuteilen, wie wir dieses Feld benennen möchten.

Verwenden von Struktur-Tags zur Steuerung der Codierung

Sie können das vorherige Beispiel ändern, um exportierte Felder zu haben, die ordnungsgemäß mit camel-cased Feldnamen codiert sind, indem Sie jedes Feld mit einem Strukturtag annotieren. Der Strukturtag, den encoding/json erkennt, hat einen Schlüssel von json und einen Wert, der die Ausgabe steuert. Indem Sie die camel-cased Version der Feldnamen als Wert für den Schlüssel json platzieren, wird der Encoder diesen Namen verwenden. Dieses Beispiel behebt die beiden vorherigen Versuche:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string    `json:"name"`
	Password      string    `json:"password"`
	PreferredFish []string  `json:"preferredFish"`
	CreatedAt     time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Dies wird ausgegeben:

Output
{ "name": "Sammy the Shark", "password": "fisharegreat", "preferredFish": null, "createdAt": "2019-09-23T18:16:17.57739-04:00" }

Wir haben die Strukturfelder wieder sichtbar gemacht für andere Pakete, indem wir die ersten Buchstaben ihrer Namen großgeschrieben haben. Dieses Mal haben wir jedoch Struktur-Tags in Form von json:"name" hinzugefügt, wobei "name" der Name war, den wir wollten, dass json.MarshalIndent verwendet, um unsere Struktur als JSON zu drucken.

Wir haben jetzt erfolgreich unser JSON korrekt formatiert. Beachten Sie jedoch, dass die Felder für einige Werte gedruckt wurden, obwohl wir diese Werte nicht gesetzt haben. Der JSON-Encoder kann diese Felder ebenfalls eliminieren, wenn Sie möchten.

Entfernen von leeren JSON-Feldern

Es ist üblich, das Ausgeben von Feldern zu unterdrücken, die in JSON nicht gesetzt sind. Da alle Typen in Go einen „Nullwert“ haben, irgendeinen Standardwert, zu dem sie gesetzt sind, benötigt das Paket encoding/json zusätzliche Informationen, um festzustellen, dass ein Feld als nicht gesetzt betrachtet werden soll, wenn es diesen Nullwert annimmt. Im Wertteil eines json-Struktur-Tags können Sie dem gewünschten Namen Ihres Feldes das Suffix ,omitempty hinzufügen, um den JSON-Encoder anzuweisen, die Ausgabe dieses Feldes zu unterdrücken, wenn das Feld auf den Nullwert gesetzt ist. Das folgende Beispiel korrigiert die vorherigen Beispiele, um keine leeren Felder mehr auszugeben:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string    `json:"name"`
	Password      string    `json:"password"`
	PreferredFish []string  `json:"preferredFish,omitempty"`
	CreatedAt     time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Dieses Beispiel wird folgendes ausgeben:

Output
{ "name": "Sammy the Shark", "password": "fisharegreat", "createdAt": "2019-09-23T18:21:53.863846-04:00" }

Wir haben die vorherigen Beispiele so geändert, dass das Feld PreferredFish jetzt das Strukturtag json:"preferredFish,omitempty" hat. Das Vorhandensein der ,omitempty-Ergänzung bewirkt, dass der JSON-Encoder dieses Feld überspringt, da wir beschlossen haben, es nicht zu setzen. In den Ausgaben unserer vorherigen Beispiele hatte dies den Wert null.

Diese Ausgabe sieht schon viel besser aus, aber wir geben immer noch das Passwort des Benutzers aus. Das Paket encoding/json bietet uns eine weitere Möglichkeit, private Felder vollständig zu ignorieren.

Ignorieren von privaten Feldern

Einige Felder müssen aus Strukturen exportiert werden, damit andere Pakete korrekt mit dem Typ interagieren können. Die Natur dieser Felder kann jedoch sensibel sein, daher möchten wir in diesen Fällen, dass der JSON-Encoder das Feld vollständig ignoriert – auch wenn es gesetzt ist. Dies wird durch die Verwendung des speziellen Werts - als Wertargument für ein json: Struktur-Tag erreicht.

Dieses Beispiel behebt das Problem der Offenlegung des Benutzerpassworts.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name      string    `json:"name"`
	Password  string    `json:"-"`
	CreatedAt time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Wenn Sie dieses Beispiel ausführen, sehen Sie diese Ausgabe:

Output
{ "name": "Sammy the Shark", "createdAt": "2019-09-23T16:08:21.124481-04:00" }

Das Einzige, was wir in diesem Beispiel gegenüber den vorherigen geändert haben, ist, dass das Passwortfeld jetzt den speziellen Wert "-" für sein json: Struktur-Tag verwendet. In der Ausgabe dieses Beispiels ist das password-Feld nicht mehr vorhanden.

Diese Funktionen des encoding/json-Pakets – ,omitempty, "-" und andere Optionen – sind keine Standards. Was ein Paket mit Werten eines Struktur-Tags macht, hängt von seiner Implementierung ab. Da das encoding/json-Paket Teil der Standardbibliothek ist, haben auch andere Pakete diese Funktionen konventionell auf die gleiche Weise implementiert. Es ist jedoch wichtig, die Dokumentation für jedes Drittanbieterpaket zu lesen, das Struktur-Tags verwendet, um zu erfahren, was unterstützt wird und was nicht.

Schlussfolgerung

Struct-Tags bieten eine leistungsstarke Möglichkeit, die Funktionalität von Code zu erweitern, der mit Ihren Strukturen arbeitet. Viele Standardbibliotheks- und Drittanbieterpakete bieten Möglichkeiten, ihre Funktionsweise durch die Verwendung von Struktur-Tags anzupassen. Ihre effektive Nutzung in Ihrem Code bietet sowohl dieses Anpassungsverhalten als auch eine prägnante Dokumentation darüber, wie diese Felder von zukünftigen Entwicklern verwendet werden.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go