Comprendre différer en Go

introduction

Go contient bon nombre des mots-clés courants utilisés dans les flux de contrôle que l’on trouve dans d’autres langages de programmation, tels que + if +, + switch +, + pour +, etc. Le mot-clé + Defer + est l’un des mots-clés que l’on ne trouve pas dans la plupart des autres langages de programmation. Bien que ce soit moins courant, vous verrez rapidement à quel point il peut être utile dans vos programmes.

L’une des principales utilisations d’une instruction + defer + est de nettoyer des ressources, telles que des fichiers ouverts, des connexions réseau et des database handles. Lorsque votre programme a terminé avec ces ressources, il est important de les fermer pour éviter d’épuiser ses limites et pour permettre aux autres programmes d’avoir accès à ces ressources. + defer + rend notre code plus propre et moins sujet aux erreurs en gardant les appels pour fermer le fichier / la ressource à proximité de l’appel ouvert.

Dans cet article, nous allons apprendre à utiliser correctement l’instruction + defer + pour nettoyer les ressources, ainsi que plusieurs erreurs courantes commises lors de l’utilisation de + defer +.

Qu’est-ce qu’une déclaration + différer +

Une déclaration + defer + ajoute l’appel function à la suite du mot clé + defer + ' empiler. Tous les appels de cette pile sont appelés lorsque la fonction dans laquelle ils ont été ajoutés est retournée. Comme les appels sont placés sur une pile, ils sont appelés dans l’ordre dernier-premier-premier sorti.

Voyons comment fonctionne + defer + en imprimant du texte:

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("Bye")
   fmt.Println("Hi")
}

Dans la fonction + main +, nous avons deux déclarations. La première déclaration commence par le mot clé + defer +, suivi par une instruction + print + qui affiche + Bye +. La ligne suivante affiche + Hi +.

Si nous exécutons le programme, nous verrons le résultat suivant:

OutputHi
Bye

Notez que + Hi + a été imprimé en premier. En effet, aucune instruction précédée du mot clé + defer + n’est invoquée jusqu’à la fin de la fonction dans laquelle + defer + a été utilisé.

Examinons à nouveau le programme et cette fois-ci, nous ajouterons quelques commentaires pour illustrer ce qui se passe:

main.go

package main

import "fmt"

func main() {
   // defer statement is executed, and places
   // fmt.Println("Bye") on a list to be executed prior to the function returning
   defer fmt.Println("Bye")

   // The next line is executed immediately
   fmt.Println("Hi")

   // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

La clé pour comprendre + defer + est que lorsque l’instruction + defer + est exécutée, les arguments de la fonction différée sont évalués immédiatement. Quand un + defer + s’exécute, il place l’instruction qui la suit sur une liste à appeler avant le retour de la fonction.

Bien que ce code illustre l’ordre dans lequel + defer + sera exécuté, ce n’est pas une façon typique de l’utiliser lors de l’écriture d’un programme Go. Il est plus probable que nous utilisions + defer + pour nettoyer une ressource, telle qu’un descripteur de fichier. Voyons comment faire cela ensuite.

Utilisation de + defer + pour nettoyer les ressources

Utiliser + defer + pour nettoyer les ressources est très courant dans Go. Examinons d’abord un programme qui écrit une chaîne dans un fichier mais n’utilise pas + defer + pour gérer le nettoyage de la ressource:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   file.Close()
   return nil
}

Dans ce programme, il existe une fonction appelée + write + qui va d’abord essayer de créer un fichier. S’il y a une erreur, il retournera l’erreur et quittera la fonction. Ensuite, il essaie d’écrire la chaîne + Ceci est un fichier readme dans le fichier spécifié. S’il reçoit une erreur, il retournera l’erreur et quittera la fonction. Ensuite, la fonction essaiera de fermer le fichier et libérera la ressource sur le système. Enfin, la fonction retourne + nil + pour indiquer que la fonction s’est exécutée sans erreur.

Bien que ce code fonctionne, il existe un bogue subtil. Si l’appel à + ​​io.WriteString + échoue, la fonction retournera sans fermer le fichier et libérer la ressource sur le système.

Nous pourrions résoudre le problème en ajoutant une autre instruction + file.Close () +, ce qui permettrait de résoudre ce problème dans une langue sans + defer +:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {

       return err
   }
   file.Close()
   return nil
}

Maintenant, même si l’appel à + ​​io.WriteString + échoue, nous allons toujours fermer le fichier. Bien que ce soit un bug relativement facile à repérer et à corriger, avec une fonction plus compliquée, il a peut-être été oublié.

Au lieu d’ajouter le deuxième appel à + ​​file.Close () +, nous pouvons utiliser une instruction + defer + pour nous assurer que, quelles que soient les branches prises lors de l’exécution, nous appelons toujours + Close () +.

Voici la version qui utilise le mot-clé + defer +:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }

   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   return nil
}

Cette fois, nous avons ajouté la ligne de code: + defer fichier.Close () +. Cela indique au compilateur qu’il doit exécuter le fichier + fichier.Fermer + avant de quitter la fonction + écriture +.

Nous nous sommes maintenant assurés que même si nous ajoutions plus de code et créons une autre branche qui quitte la fonction dans le futur, nous allons toujours nettoyer et fermer le fichier.

Cependant, nous avons introduit un autre bogue en ajoutant le différé. Nous ne vérifions plus l’erreur potentielle pouvant être renvoyée par la méthode + Close +. En effet, lorsque nous utilisons + defer +, il n’ya aucun moyen de communiquer une valeur de retour à notre fonction.

Dans Go, il est considéré comme une pratique sûre et acceptée d’appeler + Close () + plus d’une fois sans affecter le comportement de votre programme. Si + Close () + renvoie une erreur, il le fera la première fois qu’il est appelé. Cela nous permet de l’appeler explicitement dans le chemin d’exécution réussi de notre fonction.

Voyons comment nous pouvons à la fois + différer + l’appel de + Fermer +, et signaler quand même une erreur si nous en rencontrons une.

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return
}

Le seul changement dans ce programme est la dernière ligne dans laquelle nous retournons + file.Close () +. Si l’appel à + ​​Close + entraîne une erreur, celle-ci sera renvoyée comme prévu à la fonction appelante. Gardez à l’esprit que votre instruction + defer file.Close () + sera également exécutée après l’instruction + return. Cela signifie que + file.Close () + est potentiellement appelé deux fois. Bien que cela ne soit pas idéal, il s’agit d’une pratique acceptable car elle ne devrait pas entraîner d’effets secondaires pour votre programme.

Si, toutefois, nous recevons une erreur plus tôt dans la fonction, comme lorsque nous appelons + WriteString +, la fonction renvoie cette erreur et essaie également d’appeler + fichier.Fermer + car elle a été différée. Bien que + file.Close + puisse (et va probablement) renvoyer une erreur également, ce n’est plus quelque chose qui nous importe car nous avons reçu une erreur qui est plus susceptible de nous indiquer ce qui a mal tourné au début.

Jusqu’ici, nous avons vu comment nous pouvons utiliser un seul + différer + pour nous assurer de nettoyer correctement nos ressources. Nous verrons ensuite comment utiliser plusieurs instructions + defer + pour nettoyer plusieurs ressources.

Plusieurs instructions + defer +

Il est normal d’avoir plus d’une instruction + defer + dans une fonction. Créons un programme qui ne contient que des instructions + defer + pour voir ce qui se passe lorsque nous introduisons plusieurs différés:

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("one")
   defer fmt.Println("two")
   defer fmt.Println("three")
}

Si nous exécutons le programme, nous recevrons le résultat suivant:

Outputthree
two
one

Notez que l’ordre est l’inverse dans lequel nous avons appelé les instructions + defer +. En effet, chaque instruction différée appelée est empilée sur la précédente, puis appelée en sens inverse lorsque la fonction quitte la portée (Last In, First Out).

Vous pouvez avoir autant d’appels différés que nécessaire dans une fonction, mais il est important de vous rappeler qu’ils seront tous appelés dans l’ordre inverse de leur exécution.

Maintenant que nous comprenons l’ordre dans lequel plusieurs différés s’exécuteront, voyons comment nous utiliserions plusieurs différés pour nettoyer plusieurs ressources. Nous allons créer un programme qui ouvre un fichier, y écrit, puis l’ouvre à nouveau pour copier le contenu dans un autre fichier.

main.go

package main

import (
   "fmt"
   "io"
   "log"
   "os"
)

func main() {
   if err := write("sample.txt", "This file contains some sample text."); err != nil {
       log.Fatal("failed to create file")
   }

   if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
       log.Fatal("failed to copy file: %s")
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return file.Close()
}

func fileCopy(source string, destination string) error {
   src, err := os.Open(source)
   if err != nil {
       return err
   }
   defer src.Close()

   dst, err := os.Create(destination)
   if err != nil {
       return err
   }
   defer dst.Close()

   n, err := io.Copy(dst, src)
   if err != nil {
       return err
   }
   fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

   if err := src.Close(); err != nil {
       return err
   }

   return dst.Close()
}

Nous avons ajouté une nouvelle fonction appelée + fileCopy +. Dans cette fonction, nous ouvrons d’abord notre fichier source à partir duquel nous allons copier. Nous vérifions si nous avons reçu une erreur lors de l’ouverture du fichier. Si c’est le cas, nous «+ retournons +» l’erreur et quittons la fonction. Sinon, nous «reportons +» la fermeture du fichier source que nous venons d’ouvrir.

Ensuite, nous créons le fichier de destination. Encore une fois, nous vérifions si nous avons reçu une erreur lors de la création du fichier. Si c’est le cas, nous "+ retournons " cette erreur et quittons la fonction. Sinon, nous avons aussi ` différé ` le ` Fermer () ` pour le fichier de destination. Nous avons maintenant deux fonctions ` defer +` qui seront appelées lorsque la fonction quittera sa portée.

Maintenant que les deux fichiers sont ouverts, nous allons + Copy () + les données du fichier source vers le fichier de destination. Si cela réussit, nous tenterons de fermer les deux fichiers. Si nous recevons une erreur en essayant de fermer l’un ou l’autre fichier, nous allons "+ renvoyer +" l’erreur et quitter la portée de la fonction.

Notez que nous appelons explicitement + Close () + pour chaque fichier, même si + defer + appelle également + + Close () +. Cela permet de s’assurer que si une erreur survient lors de la fermeture d’un fichier, nous la signalons. Cela garantit également que si pour une raison quelconque, la fonction quitte tôt avec une erreur, par exemple si nous ne parvenons pas à copier entre les deux fichiers, chaque fichier essaiera toujours de se fermer correctement à partir des appels différés.

Conclusion

Dans cet article, nous avons appris à propos de la déclaration + defer + et de la manière dont elle peut être utilisée pour garantir le nettoyage correct des ressources système de notre programme. Si vous nettoyez correctement les ressources système, votre programme utilisera moins de mémoire et fonctionnera mieux. Pour en savoir plus sur l’utilisation de + defer +, lisez l’article sur la gestion des paniques ou explorez l’intégralité de notre How To Code. dans la série Go.