Comment utiliser les tags de structure en Go

Introduction

Les structures, ou structs, sont utilisées pour regrouper plusieurs éléments d’information ensemble dans une seule unité. Ces ensembles d’informations sont utilisés pour décrire des concepts de niveau supérieur, tels qu’une Adresse composée d’une Rue, d’une Ville, d’un État et d’un CodePostal. Lorsque vous lisez ces informations à partir de systèmes tels que des bases de données ou des API, vous pouvez utiliser des balises struct pour contrôler la manière dont ces informations sont attribuées aux champs d’une structure. Les balises struct sont de petits morceaux de métadonnées attachés aux champs d’une structure qui fournissent des instructions à d’autres codes Go qui travaillent avec la structure.

À quoi ressemble une balise struct ?

Les balises struct Go sont des annotations qui apparaissent après le type dans une déclaration de structure Go. Chaque balise est composée de courtes chaînes associées à une valeur correspondante.

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

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

Un autre code Go est ensuite capable d’examiner ces structures et d’extraire les valeurs attribuées à des clés spécifiques qu’il demande. Les balises struct n’ont aucun effet sur le fonctionnement de votre code sans code supplémentaire qui les examine.

Essayez cet exemple pour voir à quoi ressemblent les tags struct, et que sans code d’un autre package, ils n’auront aucun effet.

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

Cela produira:

Output
Hi! My name is Sammy

Cet exemple définit un type User avec un champ Name. Le champ Name s’est vu attribuer un tag struct de example:"name". Nous nous référerions à ce tag spécifique dans la conversation comme le « tag struct exemple » car il utilise le mot « example » comme clé. Le tag struct example a la valeur "name" pour le champ Name. Sur le type User, nous définissons également la méthode String() requise par l’interface fmt.Stringer. Cela sera appelé automatiquement lorsque nous passons le type à fmt.Println et nous donne l’occasion de produire une version bien formatée de notre struct.

Dans le corps de main, nous créons une nouvelle instance de notre type User et la passons à fmt.Println. Même si le struct avait un tag struct présent, nous voyons qu’il n’a aucun effet sur le fonctionnement de ce code Go. Il se comportera exactement de la même manière si le tag struct n’était pas présent.

Pour utiliser les tags struct pour accomplir quelque chose, un autre code Go doit être écrit pour examiner les structs à l’exécution. La bibliothèque standard contient des packages qui utilisent des tags struct dans le cadre de leur fonctionnement. Le plus populaire d’entre eux est le package encoding/json.

Encodage JSON

JavaScript Object Notation (JSON) est un format textuel pour encoder des collections de données organisées sous différents clés de chaîne. Il est couramment utilisé pour communiquer des données entre différents programmes car le format est assez simple pour qu’il existe des bibliothèques pour le décoder dans de nombreux langages différents. Voici un exemple de JSON:

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

Cet objet JSON contient deux clés, language et mascot. Après ces clés se trouvent les valeurs associées. Ici, la clé language a une valeur de Go et mascot est assignée à la valeur Gopher.

L’encodeur JSON dans la bibliothèque standard utilise des balises de structure comme annotations indiquant à l’encodeur comment vous souhaitez nommer vos champs dans la sortie JSON. Ces mécanismes d’encodage et de décodage JSON se trouvent dans le encoding/json package.

Essayez cet exemple pour voir comment JSON est encodé sans balises de structure:

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

Cela affichera la sortie suivante:

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

Nous avons défini une structure décrivant un utilisateur avec des champs comprenant leur nom, mot de passe et l’heure à laquelle l’utilisateur a été créé. À l’intérieur de la fonction main, nous créons une instance de cet utilisateur en fournissant des valeurs pour tous les champs sauf PreferredFish (Sammy aime tous les poissons). Nous avons ensuite passé l’instance de User à la fonction json.MarshalIndent. Ceci est utilisé afin que nous puissions voir plus facilement la sortie JSON sans utiliser d’outil de formatage externe. Cet appel pourrait être remplacé par json.Marshal(u) pour imprimer JSON sans aucun espace supplémentaire. Les deux arguments supplémentaires de json.MarshalIndent contrôlent le préfixe de la sortie (que nous avons omis avec la chaîne vide), et les caractères à utiliser pour l’indentation, qui sont ici deux caractères d’espace. Toutes les erreurs produites par json.MarshalIndent sont journalisées et le programme se termine en utilisant os.Exit(1). Enfin, nous convertissons le []byte retourné par json.MarshalIndent en une string et passons la chaîne résultante à fmt.Println pour l’impression sur le terminal.

Les champs de la structure apparaissent exactement comme ils sont nommés. Ce n’est cependant pas le style JSON typique auquel vous pourriez vous attendre, qui utilise la casse camel pour les noms des champs. Vous allez changer les noms des champs pour suivre le style camel case dans cet exemple suivant. Comme vous le verrez lorsque vous exécuterez cet exemple, cela ne fonctionnera pas car les noms de champ désirés entrent en conflit avec les règles de Go concernant les noms de champ exportés.

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

Cela produira la sortie suivante:

Output
{}

Dans cette version, nous avons modifié les noms des champs pour les écrire en camel case. Maintenant, Name est name, Password est password, et enfin CreatedAt est createdAt. À l’intérieur du corps de main, nous avons changé l’instanciation de notre structure pour utiliser ces nouveaux noms. Ensuite, nous passons la structure à la fonction json.MarshalIndent comme précédemment. Cette fois-ci, la sortie est un objet JSON vide, {}.

Pour écrire correctement les champs en camel case, le premier caractère doit être en minuscule. Bien que JSON n’ait pas d’importance pour la manière dont vous nommez vos champs, Go si, car cela indique la visibilité du champ en dehors du package. Étant donné que le package encoding/json est un package distinct du package main que nous utilisons, nous devons mettre en majuscule le premier caractère pour le rendre visible à encoding/json. Nous semblons donc être dans une impasse. Nous avons besoin d’un moyen de communiquer à l’encodeur JSON le nom que nous aimerions donner à ce champ.

Utilisation des balises struct pour contrôler l’encodage

Vous pouvez modifier l’exemple précédent pour avoir des champs exportés qui sont correctement encodés avec des noms de champs en camel case en annotant chaque champ avec une balise de structure. La balise de structure que encoding/json reconnaît a une clé de json et une valeur qui contrôle la sortie. En plaçant la version en camel case des noms de champs comme valeur de la clé json, l’encodeur utilisera ce nom à la place. Cet exemple corrige les deux tentatives précédentes:

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

Cela produira:

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

Nous avons changé les champs de la structure pour qu’ils soient visibles par d’autres packages en mettant en majuscule les premières lettres de leurs noms. Cependant, cette fois-ci, nous avons ajouté des balises de structure sous la forme de json:"nom", où "nom" était le nom que nous voulions que json.MarshalIndent utilise lors de l’impression de notre structure au format JSON.

Nous avons maintenant correctement formaté notre JSON. Remarquez cependant que les champs pour certaines valeurs ont été imprimés même si nous n’avons pas défini ces valeurs. L’encodeur JSON peut également éliminer ces champs, si vous le souhaitez.

Suppression des champs JSON vides

Il est courant de supprimer la sortie des champs non définis en JSON. Étant donné que tous les types en Go ont une « valeur zéro », une valeur par défaut à laquelle ils sont définis, le package encoding/json a besoin d’informations supplémentaires pour pouvoir indiquer qu’un champ doit être considéré comme non défini lorsqu’il assume cette valeur zéro. Dans la partie valeur de n’importe quelle balise struct json, vous pouvez suffixer le nom désiré de votre champ avec ,omitempty pour indiquer à l’encodeur JSON de supprimer la sortie de ce champ lorsque le champ est défini sur la valeur zéro. L’exemple suivant corrige les exemples précédents pour ne plus produire de champs vides :

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

Cet exemple produira :

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

Nous avons modifié les exemples précédents afin que le champ PreferredFish ait maintenant la balise struct json:"preferredFish,omitempty". La présence de l’extension ,omitempty fait que l’encodeur JSON ignore ce champ, car nous avons décidé de le laisser non défini. Cela avait la valeur null dans les sorties de nos exemples précédents.

Cette sortie a l’air beaucoup mieux, mais nous imprimons toujours le mot de passe de l’utilisateur. Le package encoding/json fournit une autre manière pour nous d’ignorer complètement les champs privés.

Ignorer les Champs Privés

Certains champs doivent être exportés à partir de structs afin que d’autres packages puissent interagir correctement avec le type. Cependant, la nature de ces champs peut être sensible, donc dans ces circonstances, nous aimerions que l’encodeur JSON ignore entièrement le champ, même lorsqu’il est défini. Cela se fait en utilisant la valeur spéciale - comme argument de valeur pour une balise struct json:.

Cet exemple corrige le problème d’exposition du mot de passe de l’utilisateur.

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

Lorsque vous exécutez cet exemple, vous verrez cette sortie :

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

La seule chose que nous avons changée dans cet exemple par rapport aux précédents est que le champ du mot de passe utilise maintenant la valeur spéciale "-" pour sa balise struct json:. Dans la sortie de cet exemple, le champ password n’est plus présent.

Ces fonctionnalités du package encoding/json,omitempty, "-", et autres options — ne sont pas des normes. Ce qu’un package décide de faire avec les valeurs d’une balise struct dépend de son implémentation. Parce que le package encoding/json fait partie de la bibliothèque standard, d’autres packages ont également implémenté ces fonctionnalités de la même manière par convention. Cependant, il est important de lire la documentation pour tout package tiers qui utilise des balises struct pour apprendre ce qui est pris en charge et ce qui ne l’est pas.

Conclusion

Les balises de struct offrent un moyen puissant d’augmenter la fonctionnalité du code qui travaille avec vos structures. De nombreuses bibliothèques standard et packages tiers proposent des moyens de personnaliser leur fonctionnement grâce à l’utilisation de balises de struct. Les utiliser efficacement dans votre code offre à la fois ce comportement de personnalisation et documente de manière succincte comment ces champs sont utilisés aux développeurs futurs.

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