Gestion des paniques dans Go

introduction

Les erreurs qu'un programme rencontre rencontrent deux grandes catégories: celles que le programmeur a anticipées et celles que le programmeur n'a pas. L'interface deerror que nous avons abordée dans nos deux articles précédents surerror handling traite en grande partie des erreurs auxquelles nous nous attendons lorsque nous écrivons des programmes Go. L'interfaceerror nous permet même de reconnaître la rare possibilité qu'une erreur se produise à partir des appels de fonction, afin que nous puissions répondre de manière appropriée dans ces situations.

Les paniques entrent dans la deuxième catégorie d'erreurs, imprévues par le programmeur. Ces erreurs imprévues conduisent un programme à se terminer spontanément et à quitter le programme Go en cours d'exécution. Les erreurs courantes sont souvent responsables de la création de paniques. Dans ce didacticiel, nous allons examiner quelques manières dont les opérations courantes peuvent générer des paniques dans Go, et nous allons également voir comment éviter ces paniques. Nous utiliserons également les instructionsdefer avec la fonctionrecover pour capturer les paniques avant qu'elles n'aient une chance de mettre fin de manière inattendue à nos programmes Go en cours d'exécution.

Comprendre la panique

Dans Go, certaines opérations renvoient automatiquement la panique et arrêtent le programme. Les opérations courantes incluent l'indexation d'unarray au-delà de sa capacité, l'exécution d'assertions de type, l'appel de méthodes sur des pointeurs nil, l'utilisation incorrecte de mutex et la tentative de travailler avec des canaux fermés. La plupart de ces situations résultent d'erreurs commises lors de la programmation que le compilateur n'a pas la capacité de détecter lors de la compilation de votre programme.

Étant donné que les paniques incluent des détails utiles pour résoudre un problème, les développeurs l'utilisent généralement pour indiquer qu'ils se sont trompés lors du développement d'un programme.

Panics hors limites

Lorsque vous essayez d'accéder à un index au-delà de la longueur d'une tranche ou de la capacité d'un tableau, l'exécution de Go génère une panique.

L'exemple suivant commet l'erreur courante de tenter d'accéder au dernier élément d'une tranche en utilisant la longueur de la tranche retournée par la fonction intégréelen. Essayez d’exécuter ce code pour voir pourquoi cela pourrait provoquer une panique:

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

Cela aura la sortie suivante:

Outputpanic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

Le nom de la sortie de panique fournit un indice:panic: runtime error: index out of range. Nous avons créé une tranche avec trois créatures marines. Nous avons ensuite essayé d'obtenir le dernier élément de la tranche en indexant cette tranche avec la longueur de la tranche à l'aide de la fonction intégréelen. N'oubliez pas que les tranches et les tableaux sont basés sur zéro; donc le premier élément est zéro et le dernier élément de cette tranche est à l'index2. Puisque nous essayons d'accéder à la tranche au troisième index,3, il n'y a aucun élément dans la tranche à renvoyer car il est au-delà des limites de la tranche. Le moteur d'exécution n'a d'autre choix que de terminer et de quitter puisque nous lui avons demandé de faire quelque chose d'impossible. Go ne peut pas non plus prouver lors de la compilation que ce code essaiera de le faire, aussi le compilateur ne pourra-t-il pas saisir cela.

Notez également que le code suivant n'a pas été exécuté. En effet, une panique est un événement qui arrête complètement l'exécution de votre programme Go. Le message produit contient plusieurs informations utiles pour diagnostiquer la cause de la panique.

Anatomie d'une panique

Les paniques sont composées d'un message indiquant la cause de la panique et d'unstack trace qui vous aide à localiser où dans votre code la panique s'est produite.

La première partie de toute panique est le message. Il commencera toujours par la chaînepanic:, qui sera suivie d'une chaîne qui varie en fonction de la cause de la panique. La panique de l'exercice précédent contient le message suivant:

panic: runtime error: index out of range [3] with length 3

La chaîneruntime error: suivant le préfixepanic: nous indique que la panique a été générée par le runtime du langage. Cette panique nous apprend que nous avons tenté d’utiliser un index[3] qui était hors de portée de la longueur de la tranche3.

Après ce message se trouve la trace de la pile. Les traces de pile forment une carte que nous pouvons suivre pour localiser exactement la ligne de code exécutée lors de la génération de la panique et comment ce code a été appelé par le code précédent.

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

Cette trace de pile, de l'exemple précédent, montre que notre programme a généré la panique à partir du fichier/tmp/sandbox879828148/prog.go à la ligne numéro 13. Il nous indique également que cette panique a été générée dans la fonctionmain() du packagemain.

La trace de pile est divisée en blocs séparés - un pour chaquegoroutine de votre programme. L’exécution de chaque programme Go est réalisée par une ou plusieurs goroutines pouvant chacune exécuter séparément et simultanément des parties de votre code Go. Chaque bloc commence par l'en-têtegoroutine X [state]:. L'en-tête donne le numéro d'identification de la goroutine ainsi que l'état dans lequel elle se trouvait lorsque la panique s'est produite. Après l’en-tête, la trace de la pile indique la fonction que le programme était en train d’exécuter lorsque la panique s’est produite, ainsi que le nom du fichier et le numéro de ligne où la fonction s’est exécutée.

La panique de l'exemple précédent a été générée par un accès hors limites à une tranche. Des paniques peuvent également être générés lorsque des méthodes sont appelées sur des pointeurs non définis.

Aucun récepteur

Le langage de programmation Go contient des pointeurs qui renvoient à une instance spécifique d’un type quelconque existant dans la mémoire de l’ordinateur au moment de l’exécution. Les pointeurs peuvent prendre la valeurnil indiquant qu'ils ne pointent sur rien. Lorsque nous tentons d'appeler des méthodes sur un pointeur qui estnil, le runtime Go générera une panique. De même, les variables qui sont des types d'interface généreront également des paniques lorsque des méthodes sont appelées. Pour voir les paniques générées dans ces cas, essayez l'exemple suivant:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

La panique produite ressemblera à ceci:

Outputpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]

goroutine 1 [running]:
main.(*Shark).SayHello(...)
    /tmp/sandbox160713813/prog.go:12
main.main()
    /tmp/sandbox160713813/prog.go:18 +0x1a

Dans cet exemple, nous avons défini une structure appeléeShark. Shark a une méthode définie sur son récepteur de pointeur appeléSayHello qui imprimera un message d'accueil en sortie standard lorsqu'il sera appelé. Dans le corps de notre fonctionmain, nous créons une nouvelle instance de cette structureShark et demandons un pointeur vers elle en utilisant l'opérateur&. Ce pointeur est affecté à la variables. Nous réaffectons ensuite la variables à la valeurnil avec l'instructions = nil. Enfin, nous essayons d'appeler la méthodeSayHello sur la variables. Au lieu de recevoir un message amical de Sammy, nous recevons une panique que nous avons tenté d'accéder à une adresse mémoire non valide. Comme la variables estnil, lorsque la fonctionSayHello est appelée, elle essaie d'accéder au champName sur le type*Shark. Comme il s’agit d’un récepteur de pointeur, et que le récepteur dans ce cas estnil, il panique car il ne peut pas déréférencer un pointeurnil.

Bien que nous ayons définis surnil explicitement dans cet exemple, en pratique, cela se produit moins évidemment. Lorsque vous voyez des paniques impliquantnil pointer dereference, assurez-vous que vous avez correctement affecté toutes les variables de pointeur que vous avez peut-être créées.

Les paniques générés à partir de zéro pointeur et d'accès hors limites sont deux paniques courantes générées par le moteur d'exécution. Il est également possible de générer manuellement une panique à l'aide d'une fonction intégrée.

Utilisation de la fonction intégréepanic

Nous pouvons également générer nos propres paniques en utilisant la fonction intégréepanic. Il faut une seule chaîne comme argument, qui est le message que la panique va produire. En règle générale, ce message est moins détaillé que la réécriture de notre code pour renvoyer une erreur. En outre, nous pouvons l’utiliser dans nos propres packages pour indiquer aux développeurs qu’ils ont peut-être commis une erreur lors de l’utilisation du code de notre package. Dans la mesure du possible, la meilleure pratique consiste à essayer de renvoyer les valeurserror aux consommateurs de notre package.

Exécutez ce code pour voir une panique générée à partir d'une fonction appelée à partir d'une autre fonction:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

La sortie de panique produite ressemble à ceci:

Outputpanic: oh no!

goroutine 1 [running]:
main.foo(...)
    /tmp/sandbox494710869/prog.go:8
main.main()
    /tmp/sandbox494710869/prog.go:4 +0x40

Ici, nous définissons une fonctionfoo qui appelle la fonction intégréepanic avec la chaîne"oh no!". Cette fonction est appelée par notre fonctionmain. Remarquez comment la sortie a le messagepanic: oh no! et la trace de pile montre une seule goroutine avec deux lignes dans la trace de pile: une pour la fonctionmain() et une pour notre fonctionfoo().

Nous avons vu que des paniques semblaient terminer notre programme là où elles étaient générées. Cela peut créer des problèmes lorsque des ressources ouvertes doivent être correctement fermées. Go fournit un mécanisme pour exécuter du code toujours, même en cas de panique.

Fonctions différées

Votre programme peut disposer de ressources qu’il doit nettoyer correctement, même pendant le traitement de la panique par le moteur d’exécution. Go vous permet de différer l'exécution d'un appel de fonction jusqu'à ce que l'exécution de la fonction appelante soit terminée. Les fonctions différées fonctionnent même en cas de panique et sont utilisées comme mécanisme de sécurité pour se prémunir contre le caractère chaotique de la panique. Les fonctions sont différées en les appelant comme d'habitude, puis en préfixant l'instruction entière avec le mot-clédefer, comme dansdefer sayHello(). Exécutez cet exemple pour voir comment un message peut être imprimé même en cas de panique:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

La sortie produite à partir de cet exemple ressemblera à:

Outputhello from the deferred function!
panic: oh no!

goroutine 1 [running]:
main.main()
    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

Dans la fonctionmain de cet exemple, nous avons d'aborddefer un appel à une fonction anonyme qui imprime le message"hello from the deferred function!". La fonctionmain produit alors immédiatement une panique en utilisant la fonctionpanic. Dans la sortie de ce programme, nous voyons d’abord que la fonction différée est exécutée et affiche son message. Vient ensuite la panique que nous avons générée enmain.

Les fonctions différées offrent une protection contre le caractère surprenant des paniques. Dans les fonctions différées, Go nous offre également l’opportunité d’empêcher une panique de mettre fin à notre programme Go à l’aide d’une autre fonction intégrée.

Traitement des paniques

Les paniques ont un seul mécanisme de récupération: la fonction intégréerecover. Cette fonction vous permet d’intercepter une panique en montant dans la pile d’appels et de l’empêcher de mettre fin inopinément à votre programme. Il a des règles strictes pour son utilisation, mais peut être inestimable dans une application de production.

Puisqu'il fait partie du packagebuiltin,recover peut être appelé sans importer de packages supplémentaires:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

Cet exemple affichera:

Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

Notre fonctionmain dans cet exemple appelle une fonction que nous définissons,divideByZero. Dans cette fonction, nousdefer un appel à une fonction anonyme chargée de gérer les paniques qui pourraient survenir lors de l'exécution dedivideByZero. Dans cette fonction anonyme différée, nous appelons la fonction intégréerecover et assignons l'erreur qu'elle renvoie à une variable. SidivideByZero panique, cette valeur deerror sera définie, sinon ce seranil. En comparant la variableerr ànil, nous pouvons détecter si une panique s'est produite, et dans ce cas nous enregistrons la panique en utilisant la fonctionlog.Println, comme s'il s'agissait de n'importe quel autreerror) s.

Suite à cette fonction anonyme différée, nous appelons une autre fonction que nous avons définie,divide, et essayons d'imprimer ses résultats en utilisantfmt.Println. Les arguments fournis amènerontdivide à effectuer une division par zéro, ce qui provoquera une panique.

Dans la sortie de cet exemple, nous voyons d'abord le message de journal de la fonction anonyme qui récupère la panique, suivi du messagewe survived dividing by zero!. Nous l'avons en effet fait, grâce à la fonction intégrée derecover arrêtant une panique autrement catastrophique qui mettrait fin à notre programme Go.

La valeurerr renvoyée parrecover() est exactement la valeur qui a été fournie à l'appel àpanic(). Il est donc essentiel de s'assurer que la valeur deerr n'est nulle que lorsqu'aucune panique ne s'est produite.

Détection des paniques avecrecover

La fonctionrecover s'appuie sur la valeur de l'erreur pour déterminer si une panique s'est produite ou non. Puisque l'argument de la fonctionpanic est une interface vide, il peut être de n'importe quel type. La valeur zéro pour tout type d'interface, y compris l'interface vide, estnil. Il faut prendre soin d'éviternil comme argument depanic comme le montre cet exemple:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

Cela produira:

Outputwe survived dividing by zero!

Cet exemple est le même que l'exemple précédent impliquantrecover avec quelques légères modifications. La fonctiondivide a été modifiée pour vérifier si son diviseur,b, est égal à0. Si tel est le cas, il générera une panique en utilisant la fonction intégréepanic avec un argument denil. La sortie, cette fois, n'inclut pas le message de journal indiquant qu'une panique s'est produite même si une a été créée pardivide. Ce comportement silencieux est la raison pour laquelle il est très important de s'assurer que l'argument de la fonction intégréepanic n'est pasnil.

Conclusion

Nous avons vu un certain nombre de façons dontpanic+`s can be created in Go and how they can be recovered from using the `+recover intégré. Bien que vous n'utilisiez pas nécessairementpanic vous-même, une bonne récupération après panique est une étape importante pour préparer les applications Go à la production.

Vous pouvez également explorerour entire How To Code in Go series.