Hoe u struct tags gebruikt in Go

Introductie

Structuren, of structs, worden gebruikt om meerdere stukken informatie samen te brengen in één eenheid. Deze verzamelingen van informatie worden gebruikt om hogere concepten te beschrijven, zoals een Adres bestaande uit een Straat, Stad, Staat, en Postcode. Wanneer je deze informatie leest van systemen zoals databases, of API’s, kan je struct-tags gebruiken om te controleren hoe deze informatie wordt toegewezen aan de velden van een structuur. Struct-tags zijn kleine stukjes metadata die aan velden van een structuur worden toegevoegd en instructies geven aan andere Go-code die met de structuur werkt.

Hoe Ziet een Struct-Tag eruit?

Go-struct-tags zijn annotaties die verschijnen na het type in een Go-structuurverklaring. Elke tag is samengesteld uit korte strings die zijn geassocieerd met een overeenkomstige waarde.

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

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

Andere Go-code is dan in staat om deze structuren te onderzoeken en de waarden die aan specifieke sleutels zijn toegewezen, te extraheren. Struct-tags hebben geen effect op de werking van je code zonder aanvullende code die ze onderzoekt.

Probeer dit voorbeeld om te zien hoe struct tags eruit zien, en dat zonder code van een ander pakket, ze geen effect zullen hebben.

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)
}

Dit zal uitvoeren:

Output
Hi! My name is Sammy

Dit voorbeeld definieert een User-type met een Name-veld. Het Name-veld heeft een struct-tag gekregen van example:"name". We zouden deze specifieke tag in een gesprek verwijzen als de “voorbeeld struct-tag” omdat het het woord “voorbeeld” gebruikt als zijn sleutel. De example struct-tag heeft de waarde "name" voor het Name-veld. Op het User-type definiëren we ook de String()-methode die vereist is door de fmt.Stringer-interface. Dit wordt automatisch opgeroepen wanneer we het type doorgeven aan fmt.Println en geeft ons de kans om een mooi opgemaakte versie van onze struct te produceren.

In het lichaam van main maken we een nieuw exemplaar van ons User-type en geven het door aan fmt.Println. Ook al had de struct een struct-tag aanwezig, we zien dat het geen effect heeft op de werking van deze Go-code. Het zal exact hetzelfde gedragen als de struct-tag niet aanwezig was.

Om struct-tags te gebruiken om iets te bereiken, moet andere Go-code worden geschreven om structs op runtime te onderzoeken. De standaardbibliotheek heeft pakketten die struct-tags gebruiken als onderdeel van hun werking. De populairste hiervan is het encoding/json-pakket.

JSON codering

JavaScript Object Notation (JSON) is een tekstueel formaat voor het coderen van verzamelingen gegevens georganiseerd onder verschillende string sleutels. Het wordt vaak gebruikt om gegevens te communiceren tussen verschillende programma’s omdat het formaat eenvoudig genoeg is dat bibliotheken bestaan om het in veel verschillende talen te decoderen. Het volgende is een voorbeeld van JSON:

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

Deze JSON-object bevat twee sleutels, language en mascot. Na deze sleutels volgen de bijbehorende waarden. Hier heeft de language-sleutel een waarde van Go en mascot is toegewezen aan de waarde Gopher.

De JSON-encoder in de standaardbibliotheek maakt gebruik van struct-tags als annotaties die aan de encoder aangeven hoe u uw velden in de JSON-uitvoer wilt noemen. Deze JSON-encoderings- en decoderingsmechanismen zijn te vinden in het encoding/json pakket.

Probeer dit voorbeeld om te zien hoe JSON wordt gecodeerd zonder struct-tags:

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))
}

Dit zal de volgende uitvoer opleveren:

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

We hebben een struct gedefinieerd die een gebruiker beschrijft met velden zoals hun naam, wachtwoord en de tijd waarop de gebruiker is aangemaakt. Binnen de main-functie maken we een instantie van deze gebruiker door waarden op te geven voor alle velden behalve PreferredFish (Sammy houdt van alle vis). Vervolgens hebben we de instantie van User doorgegeven aan de json.MarshalIndent-functie. Dit wordt gebruikt zodat we de JSON-uitvoer gemakkelijker kunnen zien zonder een extern opmaakhulpmiddel te gebruiken. Deze oproep kan worden vervangen door json.Marshal(u) om JSON af te drukken zonder extra witruimte. De twee extra argumenten voor json.MarshalIndent regelen het voorvoegsel voor de uitvoer (die we hebben weggelaten met de lege string) en de tekens die moeten worden gebruikt voor inspringen, die hier twee spatiekarakters zijn. Eventuele fouten die worden geproduceerd door json.MarshalIndent worden gelogd en het programma wordt afgesloten met os.Exit(1). Tot slot, casten we de []byte geretourneerd door json.MarshalIndent naar een string en geven we de resulterende string door aan fmt.Println voor het afdrukken op het terminalscherm.

De velden van de struct komen precies overeen met de naam. Dit is echter niet de typische JSON-stijl die je zou verwachten, die camel casing gebruikt voor de namen van velden. In het volgende voorbeeld wijzig je de namen van de velden om de camel case-stijl te volgen. Zoals je zult zien wanneer je dit voorbeeld uitvoert, zal dit niet werken omdat de gewenste veldnamen in conflict komen met Go’s regels over geëxporteerde veldnamen.

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))
}

Dit zal de volgende uitvoer presenteren:

Output
{}

In deze versie hebben we de namen van de velden gewijzigd naar camel case. Nu is Name name, Password is password, en tot slot is CreatedAt createdAt. Binnen de body van main hebben we de instantiëring van onze struct aangepast om deze nieuwe namen te gebruiken. Vervolgens geven we de struct door aan de functie json.MarshalIndent zoals eerder. De uitvoer is dit keer een leeg JSON-object, {}.

Het correct gebruiken van camel casing voor velden vereist dat het eerste karakter een kleine letter is. Hoewel JSON niet geeft om de naamgeving van velden, doet Go dat wel, omdat het de zichtbaarheid van het veld buiten het pakket aangeeft. Aangezien het encoding/json-pakket een afzonderlijk pakket is van het main-pakket dat we gebruiken, moeten we het eerste karakter verhogen om het zichtbaar te maken voor encoding/json. Het lijkt erop dat we in een impasse zitten. We hebben op de een of andere manier nodig om aan de JSON-encoder door te geven hoe we willen dat dit veld wordt genoemd.

Het Gebruik van Struct Tags om Encoding te Controleren

U kunt het vorige voorbeeld aanpassen om geëxporteerde velden te hebben die correct zijn gecodeerd met camel-cased veldnamen door elk veld te annoteren met een struct-tag. De struct-tag die encoding/json herkent heeft een sleutel van json en een waarde die de uitvoer regelt. Door de camel-cased versie van de veldnamen als waarde aan de json-sleutel toe te voegen, zal de encoder die naam in plaats daarvan gebruiken. Dit voorbeeld lost de vorige twee pogingen op:

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))
}

Dit zal uitvoeren:

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

We hebben de struct-velden terug veranderd om zichtbaar te zijn voor andere pakketten door de eerste letters van hun namen te kapitaliseren. Echter, dit keer hebben we struct-tags toegevoegd in de vorm van json:"naam", waarbij "naam" de naam was die we wilden dat json.MarshalIndent zou gebruiken bij het afdrukken van onze struct als JSON.

We hebben nu met succes onze JSON correct opgemaakt. Merk echter op dat de velden voor sommige waarden zijn afgedrukt, ook al hebben we die waarden niet ingesteld. De JSON-encoder kan deze velden ook elimineren, als u dat wilt.

Lege JSON-velden verwijderen

Het is gebruikelijk om het uitvoeren van velden die niet zijn ingesteld in JSON te onderdrukken. Aangezien alle typen in Go een “nulwaarde” hebben, een of andere standaardwaarde waarnaar ze zijn ingesteld, heeft het encoding/json pakket aanvullende informatie nodig om te kunnen bepalen dat sommige velden als niet ingesteld moeten worden beschouwd wanneer het deze nulwaarde aanneemt. Binnen het waardegedeelte van een willekeurige json struct-tag, kun je het gewenste naam van je veld achtervoegen met ,omitempty om de JSON-encoder te vertellen de uitvoer van dit veld te onderdrukken wanneer het veld is ingesteld op de nulwaarde. Het volgende voorbeeld lost de vorige voorbeelden op om geen lege velden meer uit te voeren:

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))
}

Dit voorbeeld zal uitvoeren:

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

We hebben de vorige voorbeelden aangepast, zodat het veld PreferredFish nu de struct-tag json:"preferredFish,omitempty" heeft. De aanwezigheid van de ,omitempty toevoeging zorgt ervoor dat de JSON-encoder dat veld overslaat, omdat we hebben besloten het niet in te stellen. Dit had de waarde null in de uitvoer van onze vorige voorbeelden.

Deze uitvoer ziet er veel beter uit, maar we drukken nog steeds het wachtwoord van de gebruiker af. Het encoding/json pakket biedt ons nog een manier om privévelden volledig te negeren.

Privévelden negeren

Sommige velden moeten worden geëxporteerd uit structs zodat andere pakketten correct kunnen communiceren met het type. De aard van deze velden kan echter gevoelig zijn, dus in deze omstandigheden willen we dat de JSON-encoder het veld volledig negeert, zelfs wanneer het is ingesteld. Dit wordt gedaan door de speciale waarde - te gebruiken als de waarde-argument voor een json: struct-tag.

Dit voorbeeld lost het probleem op van het blootstellen van het wachtwoord van de gebruiker.

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))
}

Wanneer je dit voorbeeld uitvoert, zie je deze uitvoer:

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

Het enige wat we hebben veranderd in dit voorbeeld ten opzichte van eerdere voorbeelden is dat het wachtwoordveld nu de speciale waarde "-" gebruikt voor zijn json: struct-tag. In de uitvoer van dit voorbeeld is te zien dat het veld wachtwoord niet langer aanwezig is.

Deze functies van het encoding/json-pakket — ,omitempty, "-", en andere opties — zijn geen standaarden. Wat een pakket besluit te doen met waarden van een struct-tag hangt af van de implementatie ervan. Omdat het encoding/json-pakket deel uitmaakt van de standaardbibliotheek, hebben andere pakketten deze functies ook op dezelfde manier geïmplementeerd als een kwestie van conventie. Het is echter belangrijk om de documentatie voor elk externe pakket dat struct-tags gebruikt door te lezen om te leren wat wordt ondersteund en wat niet.

Conclusie

Struct tags bieden een krachtig middel om de functionaliteit van code die werkt met je structuren uit te breiden. Veel standaardbibliotheken en externe pakketten bieden manieren om hun werking aan te passen door het gebruik van struct tags. Het effectief gebruiken ervan in je code biedt zowel deze aanpasbaarheid als beknopt documenteert hoe deze velden worden gebruikt voor toekomstige ontwikkelaars.

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