Les types complexes et la programmation structurée
Structures (struct) : définir vos propres types
Les structures permettent de créer des types personnalisés en regroupant plusieurs champs sous une seule entité. Elles facilitent l’organisation des données et rendent le code plus lisible et structuré, en particulier lorsqu’il s’agit de modéliser des objets ayant plusieurs propriétés. Par exemple, elle peut représenter une personne avec ses propriétés (nom, prénom, âge, etc.).
1. Déclaration d’une structure
Une structure définit un ensemble de champs regroupés sous un même type afin de représenter des données de manière cohérente. Chaque champ possède un nom et un type spécifique, permettant de concevoir des modèles adaptés aux besoins du programme tout en assurant une gestion claire et efficace des informations.
a. Définition et visibilité des structures
Les structures se déclarent en utilisant le mot-clé type, suivi du nom de la structure et du mot-clé struct. Elles regroupent plusieurs champs aux types variés pour représenter un ensemble cohérent de données.
// Déclaration d'une structure représentant une personne
type Person struct {
FirstName string // Prénom de la personne
LastName string // Nom de famille
Age int // Âge en années
Active bool // Statut actif ou non
}
La visibilité des champs dépend de la première lettre de leur nom. Un champ dont le nom commence par une majuscule est exporté et accessible en dehors du package, tandis qu’un champ en minuscule est privé et limité à son package d’origine.
// Champ exporté
type PublicExample struct {
ExportedField string
}
// Champ non exporté
type PrivateExample struct {
privateField int
}
b. Structures et types personnalisés
Les structures peuvent contenir...
Interfaces : concepts clés et polymorphisme
Les interfaces sont un élément fondamental du langage Go. Elles permettent de définir des comportements attendus sans imposer une hiérarchie de classes, offrant ainsi un puissant mécanisme de polymorphisme.
1. Définition d’une interface
En Go, une interface est un type qui définit un ensemble de méthodes qu’un type concret doit implémenter. Cela permet d’écrire du code plus flexible et modulaire en se basant sur le comportement attendu plutôt que sur une structure de données spécifique.
a. Déclaration d’une interface
Une interface est déclarée en utilisant le mot-clé type suivi d’un nom et d’un bloc contenant une ou plusieurs signatures de méthode.
// Définition d'une interface pour des formes géométriques
type Shape interface {
Area() float64 // Méthode pour calculer l'aire
}
Dans cet exemple, toute structure qui implémente une méthode Area retournant un float64 sera reconnue comme une Shape.
b. Rôle des interfaces dans Go
Contrairement à d’autres langages, Go ne nécessite pas d’héritage explicite des interfaces. Un type satisfait une interface dès qu’il implémente toutes ses méthodes, sans déclaration explicite.
// Déclaration d'une structure
type Circle struct {
Radius float64
}
// Implémentation implicite de l'interface Shape
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
Ici, la structure Circle satisfait automatiquement l’interface Shape car elle implémente la méthode Area(). Cette implémentation implicite permet d’ajouter facilement des comportements à des types existants sans modifier leur déclaration initiale.
c. Interfaces avec plusieurs méthodes
Une interface peut regrouper plusieurs méthodes pour définir des comportements plus complexes.
// Interface avec plusieurs méthodes
type Geometry interface {
Area() float64
Perimeter() float64 ...Pointeurs : comprendre les bases de la gestion mémoire
Les pointeurs sont un concept essentiel en Go, permettant de manipuler directement des adresses mémoire. Ils optimisent la gestion des données et améliorent les performances en évitant des copies inutiles.
1. Déclaration et utilisation des pointeurs
L’utilisation des pointeurs est essentielle pour manipuler directement la mémoire et optimiser la gestion des données. Ils permettent d’éviter la duplication inutile des variables et d’améliorer les performances dans certaines situations.
a. Déclaration d’un pointeur
Un pointeur est une variable qui stocke l’adresse mémoire d’une autre variable. Pour le déclarer, on utilise l’opérateur * devant le type de la variable pointée.
// Déclaration d'un pointeur vers un entier
var ptr *int
Un pointeur non initialisé contient nil, ce qui signifie qu’il ne pointe vers aucune adresse mémoire valide.
if ptr == nil {
fmt.Println("Le pointeur est nil, il ne pointe vers rien.")
}
b. Initialisation d’un pointeur
Pour initialiser un pointeur, il faut lui attribuer l’adresse d’une variable existante à l’aide de l’opérateur &.
// Déclaration et affectation d'une variable
num := 42
// Création d'un pointeur stockant l'adresse de num
ptr = &num
fmt.Println("Adresse mémoire de num :", &num)
fmt.Println("Valeur stockée dans ptr (adresse de num) :", ptr)
c. Accès et modification via un pointeur
Un pointeur permet d’accéder à la valeur d’une variable et de la modifier directement en mémoire.
// Modification de la valeur pointée
*ptr = 100
fmt.Println("Nouvelle valeur de num :", num) // Affiche 100
L’opérateur * permet de déréférencer un pointeur, c’est-à-dire d’accéder à la valeur contenue à l’adresse pointée.
d. Passage de pointeurs en argument de fonction
L’utilisation de pointeurs dans les fonctions permet de modifier directement les valeurs sans créer de copies inutiles.
// Fonction qui modifie une variable...Gestion des erreurs : philosophie de Go et bonnes pratiques
La gestion des erreurs en Go repose sur une approche simple et explicite, évitant l’usage d’exceptions comme dans d’autres langages comme Java ou Python. Go privilégie le retour de valeurs d’erreur, facilitant ainsi le contrôle du flux d’exécution et rendant le code plus lisible et prévisible.
1. Principe de gestion des erreurs en Go
La gestion des erreurs repose sur une approche explicite où les erreurs sont retournées comme des valeurs, plutôt que d’utiliser des exceptions comme dans d’autres langages. Cette méthode encourage une vérification systématique des erreurs et améliore la robustesse du code.
a. Retour d’erreur avec le type error
Le type error est utilisé pour signaler qu’une fonction peut échouer. Une convention courante est de retourner une valeur normale accompagnée d’une valeur de type error.
import (
"errors"
"fmt"
)
// Fonction qui tente de diviser deux nombres et retourne
une erreur en cas de problème
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division par zéro non autorisée")
}
return a / b, nil
}
Dans cet exemple, si b est égal à zéro, la fonction retourne une erreur explicite au lieu de provoquer un arrêt brutal.
b. Vérification et traitement des erreurs
Toute fonction retournant une erreur doit voir son retour vérifié avant d’utiliser la valeur produite.
result, err := Divide(10, 0)
if err != nil {
fmt.Println("Erreur détectée :", err)
} else {
fmt.Println("Résultat de la division :", result)
}
Ignorer les erreurs peut entraîner des comportements imprévisibles. Il est donc recommandé de toujours traiter les erreurs retournées.
c. Création d’erreurs personnalisées
Le package errors permet de définir des erreurs...