Comment utiliser les balises Struct dans Go

introduction

Les structures, ou structures, sont utilisées pour collecter plusieurs informations en une seule unité. Cescollections of information sont utilisés pour décrire des concepts de niveau supérieur, comme unAddress composé deStreet,City,State etPostalCode . 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 affectées aux champs d'une structure. Les balises Struct sont de petites métadonnées liées aux champs d'une structure qui fournissent des instructions à d'autres codes Go fonctionnant avec la structure.

À quoi ressemble un tag Struct?

Les balises de structure 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.

Une balise struct ressemble à ceci, avec le décalage de la balise avec le backtick+\ + `caractères:

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

Other Go code est alors 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 un autre code qui les examine.

Essayez cet exemple pour voir à quoi ressemblent les balises struct, et que sans code d'un autre paquet, elles 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:

OutputHi! My name is Sammy

Cet exemple définit un typeUser avec un champName. Le champName a reçu une balise struct deexample:"name". Nous appellerions cette balise spécifique dans la conversation le «exemple de balise struct», car elle utilise le mot «exemple» comme clé. La balise structexample a la valeur"name" pour le champName. Sur le typeUser, nous définissons également la méthodeString() requise par l'interfacefmt.Stringer. Cela sera appelé automatiquement lorsque nous passerons le type àfmt.Println et nous donnera une chance de produire une version bien formatée de notre structure.

Dans le corps demain, nous créons une nouvelle instance de notre typeUser et la passons àfmt.Println. Même si la structure avait une balise struct, nous voyons que cela n’a aucun effet sur le fonctionnement de ce code Go. Il se comportera exactement de la même manière si la balise struct n'était pas présente.

Pour utiliser des balises de 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 balises struct dans le cadre de leurs opérations. Le plus populaire d'entre eux est le packageencoding/json.

Encodage JSON

JavaScript Object Notation (JSON) est un format textuel destiné à coder des collections de données organisées sous différentes clés de chaîne. Il est couramment utilisé pour communiquer des données entre différents programmes car le format est suffisamment simple pour que des bibliothèques existent pour le décoder dans de nombreuses langues. Voici un exemple de JSON:

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

Cet objet JSON contient deux clés,language etmascot. Après ces clés sont les valeurs associées. Ici, la clélanguage a une valeur deGo etmascot reçoit la valeurGopher.

L'encodeur JSON de la bibliothèque standard utilise les balises struct comme des annotations indiquant à l'encodeur comment vous souhaitez nommer vos champs dans la sortie JSON. Ces mécanismes de codage et de décodage JSON se trouvent dans lesencoding/jsonpackage.

Essayez cet exemple pour voir comment JSON est codé sans balises struct:

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

Ceci imprimera 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 son nom, son mot de passe et l'heure à laquelle l'utilisateur a été créé. Dans la fonctionmain, nous créons une instance de cet utilisateur en fournissant des valeurs pour tous les champs saufPreferredFish (Sammy aime tous les poissons). Nous avons ensuite passé l'instance deUser à la fonctionjson.MarshalIndent. Ceci est utilisé afin que nous puissions voir plus facilement la sortie JSON sans utiliser un outil de formatage externe. Cet appel pourrait être remplacé parjson.Marshal(u) pour recevoir JSON sans espace supplémentaire. Les deux arguments supplémentaires dejson.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 espaces. Toutes les erreurs produites à partir dejson.MarshalIndent sont enregistrées et le programme se termine en utilisantos.Exit(1). Enfin, nous convertissons les[]byte renvoyés dejson.MarshalIndent enstring et transmettons la chaîne résultante àfmt.Println pour l'impression sur le terminal.

Les champs de la structure apparaissent exactement comme nous les avons nommés. Ce n'est pas le style JSON typique auquel vous pouvez vous attendre, qui utilise un boîtier de chameau pour les noms de champs. Vous allez modifier les noms du champ pour suivre le style de casse de chameau dans cet exemple suivant. Comme vous le verrez lorsque vous exécuterez cet exemple, cela ne fonctionnera pas car les noms de champs souhaités sont en conflit avec les règles de Go relatives aux noms de champs 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))
}

Ceci présentera la sortie suivante:

Output{}

Dans cette version, nous avons modifié les noms des champs à traiter. MaintenantName estname,Password estpassword et enfinCreatedAt estcreatedAt. Dans le corps demain, nous avons modifié l’instanciation de notre structure pour utiliser ces nouveaux noms. Nous passons ensuite la structure à la fonctionjson.MarshalIndent comme précédemment. La sortie, cette fois est un objet JSON vide,{}.

Les champs de chameaux nécessitent correctement que le premier caractère soit en minuscule. Bien que JSON ne se soucie pas de la façon dont vous nommez vos champs, Go le fait, car il indique la visibilité du champ en dehors du package. Le packageencoding/json étant un package distinct du packagemain que nous utilisons, nous devons mettre en majuscule le premier caractère afin de le rendre visible parencoding/json. Il semblerait que nous nous trouvions dans une impasse et nous avons besoin d’un moyen de transmettre au codeur JSON ce que nous aimerions que ce champ soit nommé.

Utilisation de balises Struct pour contrôler le codage

Vous pouvez modifier l'exemple précédent pour avoir des champs exportés correctement codés avec des noms de champs en camel casel en annotant chaque champ avec une balise struct. La balise struct queencoding/json reconnaît a une clé dejson et une valeur qui contrôle la sortie. En plaçant la version camel-cased des noms de champ 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 modifié les noms de champs pour les rendre visibles aux autres packages en mettant en majuscule les premières lettres de leurs noms. Cependant, cette fois, nous avons ajouté des balises de structure sous la forme dejson:"name", où"name" était le nom que nous voulions quejson.MarshalIndent utilise lors de l'impression de notre structure au format JSON.

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

Suppression des champs JSON vides

Le plus souvent, nous souhaitons supprimer les champs en sortie qui ne sont pas définis en JSON. Étant donné que tous les types de Go ont une «valeur zéro», une valeur par défaut sur laquelle ils sont définis, le packageencoding/json a besoin d'informations supplémentaires pour pouvoir indiquer que certains champs doivent être considérés comme non définis lorsqu'il prend cette valeur nulle. Dans la partie valeur de toute balise structjson, vous pouvez suffixer le nom souhaité 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 générer 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 affichera:

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 champPreferredFish ait désormais la balise structjson:"preferredFish,omitempty". La présence de l'augmentation,omitempty oblige l'encodeur JSON à ignorer ce champ, car nous avons décidé de le laisser non défini. Cela avait la valeurnull dans les sorties de nos exemples précédents.

Cette sortie semble beaucoup mieux, mais nous sommes toujours en train d’imprimer le mot de passe de l’utilisateur. Le packageencoding/json nous offre un autre moyen d'ignorer entièrement les champs privés.

Ignorer les champs privés

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

Cet exemple résout le problème de la divulgation 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 modifiée dans cet exemple par rapport aux précédents est que le champ de mot de passe utilise désormais la valeur spéciale"-" pour sa balise structjson:. Nous voyons que dans la sortie de cet exemple que le champpassword n'est plus présent.

Ces fonctionnalités du packageencoding/json,,omitempty et"-", ne sont pas des standards. Ce qu’un paquet décide de faire avec les valeurs d’une balise struct dépend de son implémentation. Comme le packageencoding/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 de tout package tiers utilisant des balises struct pour savoir ce qui est pris en charge et ce qui ne l’est pas.

Conclusion

Les balises Struct constituent un moyen puissant d’accroître les fonctionnalités du code qui fonctionne avec vos struct. De nombreux packages de bibliothèques standard et tiers offrent des moyens de personnaliser leurs opérations à l'aide de balises struct. Leur utilisation efficace dans votre code fournit à la fois ce comportement de personnalisation et documente de manière succincte la manière dont ces champs sont utilisés par les futurs développeurs.