Gestion des incidents, panique et reprise

Cet article est inspiré de celui d'Andrew Gerrand: Defer, Panic et Recover (en anglais).

defer

L'instruction defer ajoute un appel de fonction à une liste (c'est une sorte de liste de choses à faire en fin de fonction). Cette liste est traitée à la fin de l’exécution de la fonction (fonction dans laquelle plusieurs instructions defer ont pu être utilisées). defer est utilisée pour simplifier l’écriture des fonctions qui effectuent des actions de nettoyage après leur exécution.
Voici un exemple d'une fonction qui ouvre deux fichiers et copie le contenu d’un fichier dans l’autre :
func CopyFile(dstName, srcName string) (written int64, err os.Error) {
 src, err := os.Open(srcName, os.O_RDONLY, 0)
 if err != nil {
   return 
 }

 dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644)
 if err != nil {
   return
 }
 written, err = io.Copy(dst, src)
 dst.Close()
 src.Close()
 return 
}
Le problème potentiel est que si le second appel à os.Open échoue, la fonction se terminera sans avoir fermé le fichier source. On peut y remédier en ajoutant un appel src.Close() avant la deuxième instruction de retour, mais si la fonction est bien plus complexe, il n'est pas dit que le développeur ait pensé à tous les cas de figure. L’instruction defer, nous permet de fermer les fichiers à tous les coups.
func CopyFile(dstName, srcName string) (written int64, err os.Error) {
 src, err := os.Open(srcName, os.O_RDONLY, 0)
 if err != nil {
   return
 }
 defer src.Close()

 dst, err := os.Open(dstName, os.O_WRONLY|os.O_CREATE, 0644)
 if err != nil {
   return
 }
 defer dst.Close()
 return io.Copy(dst, src)
}
Quel que soit le nombre d’instructions return dans la fonction, l’instruction defer fermera tous les fichiers à la fin d'exécution de la fonction. Le comportement du defer est prédictible :
  1. Les arguments d’une fonction deferred sont évalués quand l’instruction defer est elle-même évaluée. Dans cet exemple, l’expression i est évaluée quand l’appel à Println est programmé pour plus tard (deferred ou inséré dans la liste des choses à faire à la fin de l'exécution de la fonction). Cet exemple de code affichera 0 (à la fin de la fonction).
  2. func a() {
      i := 0
      defer fmt.Println(i)
      i++
      return
    }
    
  3. Les appels à des fonctions deferred sont exécutés en LIFO (Last Input First Output ou dernier empilé, premier dépilé). Cet exemple de code affichera 3210 (à la fin de la fonction).
  4. func b() {
     for i := 0; i < 4; i++ {
       defer fmt.Print(i)
     }
    }
    
  5. Les fonctions deferred peuvent lire et affecter des valeurs de retour nommées. Dans cet exemple, une fonction deferred incrémente la valeur de retour i.
  6. func c() (i int) {
     defer func() {
       i++
     }()
     return 1
    }
    

panic

panic est une fonction native qui termine le programme, mais pas immédiatement. Lorsque l'on appelle panic dans une fonction, cette fonction s'arrête et toutes les fonctions deferred sont exécutées. Puis la fonction rend la main à son appelant qui exécute lui aussi panic. Un peu comme si l'appel à la fonction paniquons de l'exemple ci-dessous était un appel à la fonction panic.
Et ainsi de suite pour toutes les fonctions dans la pile d'exécution qui paniquent toutes les unes à la suite des autres. À la fin, le programme go finit par crasher en affichant le message passé en paramètre à la fonction panic.
On peut donc terminer une application go avec un appel à panic (dans le cas d'un comportement anormal). Le runtime go peut lui aussi faire appel à panic (et mettre un terme à votre application) en cas d'erreur telle qu'un index de tableau en dehors des bornes, etc.
Voici un exemple de code illustrant les imbrications de l'instruction defer et de la fonction panic :
func paniquons() {
	defer func(s string) {
		fmt.Println("Ce message sera affiché en dernier")
	}()
	fmt.Println("Ce message sera affiché en premier")
	panic("Erreur")
	fmt.Println("Ce message ne sera pas affiché")
}

recover

L’instruction recover est également une fonction native de Go qui redonne le contrôle sur une fonction dans laquelle panic a été appelée. Le recover est utile uniquement à l’intérieur d’une fonction retardée (deferred). Durant une exécution normale, un appel à recover retournera nil et n’aura pas d’autres effets. S'il y a un appel à panic dans une fonction, un appel à recover récupérera la valeur donnée à panic et continuera l’exécution normalement.

Exemple de programme utilisant defer, panic et recover

Voici un exemple de code combinant toutes les techniques examinées dans cet article :
package main
import "fmt"

func main() {
 f()
 fmt.Println("Retour normal de la fonction f.")
}

func f() {
 defer func() {
	if r := recover(); r != nil {
	fmt.Println("Recover appelé dans la fonction f : ", r)
	}
 }()

 fmt.Println("Appel de la fonction g.")
 g(0)
 fmt.Println("Retour normal de la fonction g.")
}

func g(i int) {
 if i > 3 {
   fmt.Println("Appel à panic")
   panic(fmt.Sprintf("%v", i)) 
 }
 defer fmt.Println("Instruction retardée (defer) dans la fonction g : ", i)
 fmt.Println("Affichage dans la fonction g : ", i)
 g(i+1)
}
La fonction g reçoit en paramètre un entier i et panique si i est plus grand que 3, dans le cas contraire la fonction continue son exécution et s'appelle elle-même avec l’argument i+1.
La fonction f fait appel à defer en implémentant une fonction qui fait appel à recover et affiche la valeur récupérée (si cette valeur n'est pas nulle). Le programme affichera :
Appel de la fonction g.
Affichage dans la fonction g : 0
Affichage dans la fonction g : 1
Affichage dans la fonction g : 2
Affichage dans la fonction g : 3
Appel à panic
Instruction retardée (defer) dans la fonction g : 3
Instruction retardée (defer) dans la fonction g : 2
Instruction retardée (defer) dans la fonction g : 1
Instruction retardée (defer) dans la fonction g : 0
Recover appelé dans la fonction f : 4
Retour normal de la fonction f.
Si nous supprimons l'appel à defer dans la fonction f, il n'y a aura pas de récupération (avec recover) et le programme sera terminé plus tôt (sans revenir dans la fonction main de notre exemple). Le programme modifié affichera :
Appel de la fonction g.
Affichage dans la fonction g : 0
Affichage dans la fonction g : 1
Affichage dans la fonction g : 2
Affichage dans la fonction g : 3
Appel à panic
Instruction retardée (defer) dans la fonction g : 3
Instruction retardée (defer) dans la fonction g : 2
Instruction retardée (defer) dans la fonction g : 1
Instruction retardée (defer) dans la fonction g : 0
panic: 4
panic PC=0x2a9cd8 [... suite de l'affichage de la pile]

D'autres exemples plus concrets

Pour avoir des exemples d'utilisation réalistes de panic et de recover, allez voir le package json de la bibliothèque standard de Go. Il décode des données JSON avec un ensemble de fonctions récursives.
Losrque un contenu JSON est mal formé, le parser appelle panic, tandis que les fonctions appelantes récupèrent de la situation de panique (avec recover) et retournent un message d'erreur approprié (cf. les fonctions error et unmarshal dans le fichier source decode.go du package json). Il existe un autre exemple de cette technique dans la routine compile du package regexp. Dans les bibliothèques Go, la convention est que lorsqu'un package utilise panic, l’api externe présente des messages d’erreur explicites.

Étiquettes :   debuter   erreur 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Comment gérer efficacement les erreurs en golang ?

Préconisations officielles pour la gestion des erreurs dans un programme golang. Cet article complète les explications sur panic, defer et recover   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Gobs le format natif d'échange de données en Go

Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif   Lire »

Comment écrire du code Go ?

Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go.   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)