1 Introduction à la fonction defer en Golang

En langage Go, l'instruction defer retarde l'exécution de l'appel de fonction suivant jusqu'à ce que la fonction contenant l'instruction defer soit sur le point de terminer son exécution. On peut le comparer au bloc finally dans d'autres langages de programmation, mais l'utilisation de defer est plus flexible et unique.

L'avantage d'utiliser defer est qu'il peut être utilisé pour effectuer des tâches de nettoyage, telles que la fermeture de fichiers, le déverrouillage de mutex ou simplement l'enregistrement de l'heure de sortie d'une fonction. Cela peut rendre le programme plus robuste et réduire la quantité de travail de programmation dans la gestion des exceptions. Dans la philosophie de conception de Go, l'utilisation de defer est recommandée car elle contribue à garder le code concis et lisible lors de la gestion des erreurs, du nettoyage des ressources et d'autres opérations ultérieures.

2 Principe de fonctionnement de defer

2.1 Principe de base de fonctionnement

Le principe de base de defer est d'utiliser une pile (principe du dernier entré, premier sorti) pour stocker chaque fonction différée à exécuter. Lorsqu'une instruction defer apparaît, le langage Go n'exécute pas immédiatement la fonction suivant l'instruction. Au lieu de cela, il la place dans une pile dédiée. Ce n'est que lorsque la fonction externe est sur le point de retourner que ces fonctions différées seront exécutées dans l'ordre de la pile, la fonction dans la dernière instruction defer déclarée étant exécutée en premier.

De plus, il convient de noter que les paramètres des fonctions suivant l'instruction defer sont calculés et fixes au moment où le defer est déclaré, et non lors de l'exécution effective.

func exemple() {
    defer fmt.Println("monde") // différée
    fmt.Println("bonjour")
}

func principal() {
    exemple()
}

Le code ci-dessus produira en sortie :

bonjour
monde

"monde" est imprimé avant que la fonction exemple se termine, même s'il apparaît avant "bonjour" dans le code.

2.2 Ordre d'exécution de plusieurs instructions defer

Lorsqu'une fonction comporte plusieurs instructions defer, elles seront exécutées selon un principe de dernier entré, premier sorti. Cela est souvent très important pour comprendre la logique de nettoyage complexe. L'exemple suivant illustre l'ordre d'exécution de plusieurs instructions defer :

func multiplesDelais() {
    defer fmt.Println("Première différée")
    defer fmt.Println("Deuxième différée")
    defer fmt.Println("Troisième différée")

    fmt.Println("Corps de la fonction")
}

func principal() {
    multiplesDelais()
}

La sortie de ce code sera :

Corps de la fonction
Troisième différée
Deuxième différée
Première différée

Étant donné que defer suit le principe du dernier entré, premier sorti, même si "Première différée" est la première différée, elle sera exécutée en dernier.

3 Applications de defer dans différents scénarios

3.1 Libération des ressources

En langage Go, l'instruction defer est couramment utilisée pour gérer la logique de libération des ressources, telles que les opérations de fichiers et les connexions de base de données. defer garantit qu'après l'exécution de la fonction, les ressources correspondantes seront correctement libérées, quelle que soit la raison de la sortie de la fonction.

Exemple d'opération de fichier :

func LireFichier(nomFichier string) {
    fichier, err := os.Open(nomFichier)
    if err != nil {
        log.Fatal(err)
    }
    // Utiliser defer pour s'assurer que le fichier est fermé
    defer fichier.Close()

    // Effectuer des opérations de lecture de fichier...
}

Dans cet exemple, une fois que os.Open ouvre avec succès le fichier, l'instruction defer fichier.Close() garantit que la ressource du fichier sera correctement fermée et que la poignée de fichier sera libérée à la fin de la fonction.

Exemple de connexion de base de données :

func InterrogerBaseDeDonnees(requete string) {
    bd, err := sql.Open("mysql", "utilisateur:motDePasse@/nomBaseDeDonnées")
    if err != nil {
        log.Fatal(err)
    }
    // Assurer la fermeture de la connexion à la base de données en utilisant defer
    defer bd.Close()

    // Effectuer des opérations de requête de base de données...
}

De même, defer bd.Close() garantit que la connexion à la base de données sera fermée à la sortie de la fonction InterrogerBaseDeDonnees, quelle que soit la raison (retour normal ou exception levée).

3.2 Opérations de verrouillage en programmation concurrente

En programmation concurrente, l'utilisation de defer pour gérer la libération de verrous mutex est une bonne pratique. Cela garantit que le verrou est correctement libéré après l'exécution du code de la section critique, évitant ainsi les interblocages.

Exemple de verrou mutex :

var mutex sync.Mutex

func updateSharedResource() {
    mutex.Lock()
    // Utilisez defer pour vous assurer que le verrou est libéré
    defer mutex.Unlock()

    // Effectuez des modifications sur la ressource partagée...
}

Peu importe si la modification de la ressource partagée réussit ou si une panique se produit entre-temps, defer s'assurera que Unlock() est appelé, permettant à d'autres goroutines en attente du verrou de l'acquérir.

Astuce : Des explications détaillées sur les verrous mutex seront couvertes dans les chapitres suivants. À ce stade, comprendre les scénarios d'application de defer est suffisant.

3 Pièges courants et considérations pour defer

Lors de l'utilisation de defer, bien que la lisibilité et la maintenabilité du code soient grandement améliorées, il y a aussi des pièges et des considérations à garder à l'esprit.

3.1 Les paramètres des fonctions différées sont évalués immédiatement

func printValue(v int) {
    fmt.Println("Valeur :", v)
}

func main() {
    valeur := 1
    defer printValue(valeur)
    // La modification de la valeur de `valeur` n'affectera pas le paramètre déjà passé à defer
    valeur = 2
}
// Le résultat sera "Valeur : 1"

Malgré le changement de la valeur de valeur après l'instruction defer, le paramètre passé à printValue dans le defer est déjà évalué et fixe, donc la sortie sera toujours "Valeur : 1".

3.2 Soyez prudent lors de l'utilisation de defer à l'intérieur des boucles

Utiliser defer à l'intérieur d'une boucle peut entraîner des ressources qui ne sont pas libérées avant la fin de la boucle, ce qui peut entraîner des fuites ou l'épuisement des ressources.

3.3 Évitez le "libérer après utilisation" en programmation concurrente

Dans les programmes concurrents, lors de l'utilisation de defer pour libérer des ressources, il est important de s'assurer que toutes les goroutines n'essaieront pas d'accéder à la ressource après sa libération, pour éviter les conditions de concurrence.

4. Notez l'ordre d'exécution des instructions defer

Les instructions defer suivent le principe du dernier entré, premier sorti (LIFO), où le defer déclaré en dernier sera exécuté en premier.

Solutions et meilleures pratiques :

  • Soyez toujours conscient que les paramètres de fonction dans les instructions defer sont évalués au moment de la déclaration.
  • Lors de l'utilisation de defer à l'intérieur d'une boucle, envisagez d'utiliser des fonctions anonymes ou d'appeler explicitement la libération des ressources.
  • Dans un environnement concurrent, assurez-vous que toutes les goroutines ont terminé leurs opérations avant d'utiliser defer pour libérer des ressources.
  • Lors de l'écriture de fonctions contenant plusieurs instructions defer, considérez soigneusement leur ordre d'exécution et leur logique.

Suivre ces meilleures pratiques peut éviter la plupart des problèmes rencontrés lors de l'utilisation de defer et conduire à l'écriture d'un code Go plus robuste et maintenable.