1 Fondamentaux de Struct

En langage Go, un struct est un type de données composite utilisé pour agréger différents types de données similaires ou différents en une seule entité. Les structs occupent une place importante en Go car ils servent d'aspect fondamental de la programmation orientée objet, bien qu'avec des différences légères par rapport aux langages de programmation orientés objet traditionnels.

Les structs sont nécessaires pour les raisons suivantes :

  • Organiser des variables ayant une forte pertinence pour améliorer la maintenabilité du code.
  • Fournir un moyen de simuler des "classes", facilitant l'encapsulation et les fonctionnalités d'agrégation.
  • Lors de l'interaction avec des structures de données telles que JSON, les enregistrements de base de données, etc., les structs offrent un outil de mappage pratique.

L'organisation des données avec des structs permet une représentation plus claire des modèles d'objets du monde réel tels que les utilisateurs, les commandes, etc.

2 Définition d'un Struct

La syntaxe pour définir un struct est la suivante :

type NomStruct struct {
    Champ1 TypeChamp1
    Champ2 TypeChamp2
    // ... autres variables membres
}
  • Le mot-clé type introduit la définition du struct.
  • NomStruct est le nom du type de struct, suivant les conventions de nommage de Go, il est généralement écrit en majuscule pour indiquer son exportabilité.
  • Le mot-clé struct indique qu'il s'agit d'un type de struct.
  • À l'intérieur des accolades {}, les variables membres (champs) du struct sont définies, chacune suivie de son type.

Le type des membres du struct peut être n'importe quel type, y compris des types de base (comme int, string, etc.) et des types complexes (comme des tableaux, des tranches, un autre struct, etc.).

Par exemple, la définition d'un struct représentant une personne :

type Personne struct {
    Nom    string
    Âge    int
    Emails []string // peut inclure des types complexes, tels que des tranches
}

Dans le code ci-dessus, le struct Personne a trois variables membres : Nom de type string, Âge de type entier et Emails de type tranche de chaînes, indiquant qu'une personne peut avoir plusieurs adresses e-mail.

3 Création et Initialisation d'un Struct

3.1 Création d'une Instance de Struct

Il existe deux façons de créer une instance de struct : par déclaration directe ou en utilisant le mot-clé new.

Déclaration directe :

var p Personne

Le code ci-dessus crée une instance p de type Personne, où chaque variable membre du struct est la valeur zéro de son type correspondant.

En utilisant le mot-clé new :

p := new(Personne)

La création d'un struct en utilisant le mot-clé new résulte en un pointeur vers le struct. La variable p à ce stade est de type *Personne, pointant vers une variable nouvellement allouée de type Personne où les variables membres ont été initialisées à des valeurs zéro.

3.2 Initialisation des Instances de Struct

Les instances de struct peuvent être initialisées en une seule fois lorsqu'elles sont créées, en utilisant deux méthodes : avec les noms de champ ou sans les noms de champ.

Initialisation avec les Noms de Champ :

p := Personne{
    Nom:    "Alice",
    Âge:    30,
    Emails: []string{"[email protected]", "[email protected]"},
}

Lors de l'initialisation avec la forme d'assignation de champ, l'ordre d'initialisation n'a pas besoin d'être le même que l'ordre de déclaration du struct, et les champs non initialisés conserveront leurs valeurs zéro.

Initialisation sans les Noms de Champ :

p := Personne{"Bob", 25, []string{"[email protected]"}}

Lors de l'initialisation sans les noms de champ, assurez-vous que les valeurs initiales de chaque variable membre sont dans le même ordre que lors de la définition du struct, et aucun champ ne peut être omis.

De plus, les structs peuvent être initialisés avec des champs spécifiques, et tout champ non spécifié prendra des valeurs zéro :

p := Personne{Nom: "Charlie"}

Dans cet exemple, seul le champ Nom est initialisé, tandis que Âge et Emails prendront tous deux leurs valeurs zéro correspondantes.

4 Accès aux Membres d'un Struct

Accéder aux variables membres d'un struct en Go est très simple, réalisé en utilisant l'opérateur point (.). Si vous avez une variable de struct, vous pouvez lire ou modifier ses valeurs membres de cette manière.

package main

import "fmt"

// Définition de la structure Personne
type Person struct {
    Name string
    Age  int
}

func main() {
    // Création d'une variable de type Personne
    p := Person{"Alice", 30}

    // Accès aux membres de la structure
    fmt.Println("Nom:", p.Name)
    fmt.Println("Âge:", p.Age)

    // Modification des valeurs des membres
    p.Name = "Bob"
    p.Age = 25

    // Accès aux valeurs modifiées des membres
    fmt.Println("\nNom mis à jour:", p.Name)
    fmt.Println("Âge mis à jour:", p.Age)
}

Dans cet exemple, nous définissons d'abord une structure Personne avec deux membres variables, Nom et Âge. Nous créons ensuite une instance de cette structure et démontrons comment lire et modifier ces membres.

5 Composition et intégration de structures

Les structures peuvent non seulement exister indépendamment, mais aussi être composées et imbriquées ensemble pour créer des structures de données plus complexes.

5.1 Structures anonymes

Une structure anonyme ne déclare pas explicitement un nouveau type, mais utilise plutôt directement la définition de la structure. Cela est utile lorsque vous avez besoin de créer une structure une fois et de l'utiliser simplement, évitant ainsi la création de types inutiles.

Example:

package main

import "fmt"

func main() {
    // Définition et initialisation d'une structure anonyme
    personne := struct {
        Nom string
        Âge int
    }{
        Nom: "Eve",
        Âge: 40,
    }

    // Accès aux membres de la structure anonyme
    fmt.Println("Nom:", personne.Nom)
    fmt.Println("Âge:", personne.Âge)
}

Dans cet exemple, au lieu de créer un nouveau type, nous définissons directement une structure et créons une instance de celle-ci. Cet exemple démontre comment initialiser une structure anonyme et accéder à ses membres.

5.2 Intégration de structures

L'intégration de structures implique l'imbriquement d'une structure en tant que membre d'une autre structure. Cela nous permet de construire des modèles de données plus complexes.

Exemple:

package main

import "fmt"

// Définir la structure Adresse
type Adresse struct {
    Ville    string
    Pays string
}

// Intégrer la structure Adresse dans la structure Personne
type Personne struct {
    Nom    string
    Âge     int
    Adresse Adresse
}

func main() {
    // Initialiser une instance de Personne
    p := Personne{
        Nom: "Charlie",
        Âge: 28,
        Adresse: Adresse{
            Ville:    "New York",
            Pays: "USA",
        },
    }

    // Accéder aux membres de la structure intégrée
    fmt.Println("Nom:", p.Nom)
    fmt.Println("Âge:", p.Âge)
    // Accéder aux membres de la structure Adresse
    fmt.Println("Ville:", p.Adresse.Ville)
    fmt.Println("Pays:", p.Adresse.Pays)
}

Dans cet exemple, nous définissons une structure Adresse et l'intégrons en tant que membre dans la structure Personne. Lors de la création d'une instance de Personne, nous créons également une instance de Adresse simultanément. Nous pouvons accéder aux membres de la structure intégrée en utilisant la notation pointée.

6 Méthodes de structures

Les fonctionnalités de la programmation orientée objet (POO) peuvent être mises en œuvre à travers les méthodes de structures.

6.1 Concepts de base des méthodes

En langage Go, bien qu'il n'y ait pas de concept traditionnel de classes et d'objets, des fonctionnalités similaires à la POO peuvent être réalisées en associant des méthodes aux structures. Une méthode de structure est un type spécial de fonction associé à un type de structure spécifique (ou à un pointeur vers une structure), ce qui permet à ce type d'avoir son propre ensemble de méthodes.

// Définir une structure simple
type Rectangle struct {
    longueur, largeur float64
}

// Définir une méthode pour la structure Rectangle pour calculer la surface du rectangle
func (r Rectangle) Surface() float64 {
    return r.longueur * r.largeur
}

6.2 Récepteurs de valeur et récepteurs de pointeurs

Les méthodes peuvent être catégorisées en tant que récepteurs de valeur et récepteurs de pointeurs en fonction du type de récepteur. Les récepteurs de valeur utilisent une copie de la structure pour appeler la méthode, tandis que les récepteurs de pointeurs utilisent un pointeur vers la structure et peuvent modifier la structure d'origine.

// Définir une méthode avec un récepteur de valeur
func (r Rectangle) Périmètre() float64 {
    return 2 * (r.longueur + r.largeur)
}

// Définir une méthode avec un récepteur de pointeur, qui peut modifier la structure
func (r *Rectangle) DéfinirLongueur(nouvelleLongueur float64) {
    r.longueur = nouvelleLongueur // peut modifier la valeur d'origine de la structure
}

Dans l'exemple ci-dessus, Périmètre est une méthode avec un récepteur de valeur, son appel ne changera pas la valeur de Rectangle. Cependant, DéfinirLongueur est une méthode avec un récepteur de pointeur, et son appel affectera l'instance originale de Rectangle.

6.3 Invocation de méthode

Vous pouvez appeler les méthodes d'une structure en utilisant la variable de structure et son pointeur.

func main() {
    rect := Rectangle{longueur: 10, largeur: 5}

    // Appeler la méthode avec un récepteur de valeur
    fmt.Println("Aire:", rect.Aire())

    // Appeler la méthode avec un récepteur de valeur
    fmt.Println("Périmètre:", rect.Périmètre())

    // Appeler la méthode avec un récepteur de pointeur
    rect.DéfinirLongueur(20)

    // Appeler à nouveau la méthode avec un récepteur de valeur, notez que la longueur a été modifiée
    fmt.Println("Après modification, Aire:", rect.Aire())
}

Lorsque vous appelez une méthode en utilisant un pointeur, Go gère automatiquement la conversion entre valeurs et pointeurs, indépendamment de savoir si votre méthode est définie avec un récepteur de valeur ou un récepteur de pointeur.

6.4 Sélection du type de récepteur

Lors de la définition des méthodes, vous devriez décider d'utiliser un récepteur de valeur ou un récepteur de pointeur en fonction de la situation. Voici quelques directives courantes :

  • Si la méthode doit modifier le contenu de la structure, utilisez un récepteur de pointeur.
  • Si la structure est grande et que le coût de la copie est élevé, utilisez un récepteur de pointeur.
  • Si vous voulez que la méthode modifie la valeur à laquelle le récepteur pointe, utilisez un récepteur de pointeur.
  • Pour des raisons d'efficacité, même si vous ne modifiez pas le contenu de la structure, il est raisonnable d'utiliser un récepteur de pointeur pour une grande structure.
  • Pour les petites structures, ou lorsque vous ne faites que lire des données sans avoir besoin de les modifier, un récepteur de valeur est souvent plus simple et plus efficace.

À travers les méthodes de structure, nous pouvons simuler certaines fonctionnalités de la programmation orientée objet en Go, telles que l'encapsulation et les méthodes. Cette approche en Go simplifie le concept d'objets tout en fournissant suffisamment de capacité pour organiser et gérer des fonctions connexes.

7 Struct et sérialisation JSON

En Go, il est souvent nécessaire de sérialiser une structure en format JSON pour une transmission réseau ou comme fichier de configuration. De même, nous devons également être en mesure de désérialiser du JSON en instances de structure. Le paquet encoding/json en Go fournit cette fonctionnalité.

Voici un exemple de conversion entre une structure et du JSON :

package main

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

// Définir la structure Personne, et utiliser des balises json pour définir la correspondance entre les champs de la structure et les noms de champ JSON
type Personne struct {
	Nom    string   `json:"name"`
	Âge    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Créer une nouvelle instance de Personne
	p := Personne{
		Nom:    "Jean Dupont",
		Âge:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// Sérialiser en JSON
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("Échec de la sérialisation JSON : %s", err)
	}
	fmt.Printf("Format JSON : %s\n", jsonData)

	// Désérialiser en une structure
	var p2 Personne
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("Échec de la désérialisation JSON : %s", err)
	}
	fmt.Printf("Structure récupérée : %#v\n", p2)
}

Dans le code ci-dessus, nous avons défini une structure Personne, comprenant un champ de type slice avec l'option "omitempty". Cette option spécifie que si le champ est vide ou manquant, il ne sera pas inclus dans le JSON.

Nous avons utilisé la fonction json.Marshal pour sérialiser une instance de structure en JSON, et la fonction json.Unmarshal pour désérialiser des données JSON en une instance de structure.

8 Sujets avancés dans les structures

8.1 Comparaison des structs

En Go, il est autorisé de comparer directement deux instances de structs, mais cette comparaison est basée sur les valeurs des champs à l'intérieur des structs. Si toutes les valeurs des champs sont égales, alors les deux instances des structs sont considérées comme égales. Il convient de noter que tous les types de champs ne peuvent pas être comparés. Par exemple, une struct contenant des tranches (slices) ne peut pas être directement comparée.

Voici un exemple de comparaison de structs :

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Sortie : p1 == p2 : vrai
fmt.Println("p1 == p3:", p1 == p3) // Sortie : p1 == p3 : faux
}

Dans cet exemple, p1 et p2 sont considérés comme égaux car toutes leurs valeurs de champs sont les mêmes. Et p3 n'est pas égal à p1 car la valeur de Y est différente.

8.2 Copie des structs

En Go, les instances de structs peuvent être copiées par affectation. Que cette copie soit une copie profonde ou une copie superficielle dépend des types des champs à l'intérieur de la struct.

Si la struct ne contient que des types de base (tels que int, string, etc.), la copie est une copie profonde. Si la struct contient des types de référence (tels que des tranches, des cartes, etc.), la copie sera une copie superficielle et l'instance originale et la nouvelle instance copiée partageront la mémoire des types de référence.

Voici un exemple de copie d'une struct :

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// Initialiser une instance de la struct Data
original := Data{Numbers: []int{1, 2, 3}}

// Copier la struct
copied := original

// Modifier les éléments de la tranche (slice) copiée
copied.Numbers[0] = 100

// Voir les éléments des instances originale et copiée
fmt.Println("Original:", original.Numbers) // Sortie : Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // Sortie : Copied: [100 2 3]
}

Comme le montre l'exemple, les instances original et copied partagent la même tranche, donc la modification des données de la tranche dans copied affectera également les données de la tranche dans original.

Pour éviter ce problème, vous pouvez réaliser une véritable copie profonde en copiant explicitement le contenu de la tranche dans une nouvelle tranche :

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

De cette façon, toute modification apportée à copied n'affectera pas original.