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...