Bonjour, chers lecteurs de Habrahabr. Tout en discutant d'une nouvelle conception possible pour la gestion des erreurs et en discutant des avantages d'une gestion explicite des erreurs, je propose d'examiner certaines des caractéristiques des erreurs, des paniques et de leur récupération dans Go qui seront utiles dans la pratique.

erreur
l'erreur est une interface. Et comme la plupart des interfaces de Go, la définition de l' erreur est courte et simple:
type error interface { Error() string }
Il s'avère que tout type doté de la méthode Error peut être utilisé comme erreur. Comme Rob Pike l'a enseigné, les erreurs sont des valeurs et les valeurs peuvent être utilisées pour manipuler et programmer diverses logiques.
Il existe deux fonctions dans la bibliothèque standard Go qui sont facilement utilisées pour créer des erreurs. La fonction errors.New est bien adaptée pour créer des erreurs simples. La fonction fmt.Errorf permet l'utilisation d'un formatage standard.
err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id)
En règle générale, le type d'erreur est suffisant pour traiter les erreurs. Mais parfois, il peut être nécessaire de transmettre des informations supplémentaires avec une erreur; dans de tels cas, vous pouvez ajouter votre propre type d'erreur.
Un bon exemple est le type de PathError du package os
La valeur d'une telle erreur contiendra l'opération, le chemin et l'erreur.
Ils sont initialisés de cette façon:
... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e}
Le traitement peut avoir une forme standard:
_, err := os.Open("---") if err != nil{ fmt.Println(err) }
Mais s'il est nécessaire d'obtenir des informations supplémentaires, vous pouvez décompresser l'erreur dans * os.PathError :
_, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) }
La même approche peut être appliquée si la fonction peut renvoyer plusieurs types d'erreurs différents.
jouer
Déclaration de plusieurs types d'erreurs, chacune a ses propres données:
code type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }
Une fonction qui peut renvoyer ces erreurs:
code func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil }
Erreur lors de la gestion des conversions de type:
code func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } }
Dans le cas où les erreurs n'ont pas besoin de propriétés spéciales, il est recommandé dans Go de créer des variables pour stocker les erreurs au niveau du package. Un exemple est des erreurs comme io.EOF, io.ErrNoProgress, etc.
Dans l'exemple ci-dessous, nous interrompons la lecture et continuons à exécuter l'application lorsque l'erreur est io.EOF ou nous fermons l'application pour toute autre erreur.
func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } }
Ceci est efficace car les erreurs sont générées une seule fois et réutilisées.
trace de pile
Liste des fonctions appelées au moment de la capture de la pile. Le traçage de pile vous aide à avoir une meilleure idée de ce qui se passe dans le système. L'enregistrement de la trace dans les journaux peut grandement aider lors du débogage.
Go manque souvent ces informations par erreur, mais heureusement, obtenir un vidage dans Go n'est pas difficile.
Vous pouvez utiliser debug.PrintStack () pour sortir la trace vers la sortie standard:
func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() }
En conséquence, les informations suivantes seront écrites à Stderr:
pile goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27
debug.Stack () renvoie une tranche d'octets avec un vidage de pile, qui peut ensuite être enregistré ou ailleurs.
b := debug.Stack() fmt.Printf("Trace:\n %s\n", b)
Il y a un autre point si nous aimons cela:
go bar()
nous obtenons ensuite les informations suivantes en sortie:
main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c
Chaque goroutine a une pile séparée, respectivement, nous n'obtenons que son vidage. Soit dit en passant, les goroutines ont leurs propres piles, la récupération est toujours liée à cela, mais plus à ce sujet plus tard.
Et donc, pour voir les informations sur tous les goroutines, vous pouvez utiliser runtime.Stack () et passer le second argument true.
func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) }
pile Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27
Ajoutez ces informations à l'erreur et augmentez ainsi considérablement son contenu.
Par exemple, comme ceci:
type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() }
Vous pouvez ajouter une fonction pour créer cette erreur:
func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} }
Ensuite, vous pouvez déjà travailler avec ceci:
func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err }
pile Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29
En conséquence, l'erreur et la trace peuvent être divisées:
func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } }
Et bien sûr, il existe déjà une solution toute faite. L'un d'eux est le package https://github.com/pkg/errors . Il vous permet de créer une nouvelle erreur, qui contiendra déjà la pile de trace, et vous pouvez ajouter une trace et / ou un message supplémentaire à une erreur existante. Plus un formatage de sortie pratique.
import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") }
pile error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361
% v n'affichera que les messages
error2: error
paniquer / récupérer
La panique (aka accident, aka panic), en règle générale, signale la présence de dysfonctionnements, en raison desquels le système (ou un sous-système spécifique) ne peut pas continuer à fonctionner. Si la panique est appelée, le runtime Go examine la pile, essayant de trouver un gestionnaire pour celle-ci.
Les paniques non traitées mettent fin à l'application. Cela les distingue fondamentalement des erreurs qui vous permettent de ne pas vous traiter vous-même.
Vous pouvez passer n'importe quel argument à l'appel de fonction de panique.
panic(v interface{})
Il est pratique de paniquer pour transmettre une erreur du type qui simplifie la récupération et facilite le débogage.
panic(errors.New("error"))
La reprise après sinistre dans Go est basée sur un appel de fonction différée, également appelé différer . Une telle fonction est garantie d'être exécutée au retour de la fonction parent. Quelle que soit la raison - l'instruction return, la fin de la fonction ou la panique.
Et maintenant, la fonction de récupération permet d'obtenir des informations sur l'accident et d'arrêter le déroulement de la pile d'appels.
Un appel de panique typique et un gestionnaire:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) }
récupérer renvoie l'interface {} (celle que nous passons à la panique) ou nil s'il n'y a pas eu d'appel à la panique.
Prenons un autre exemple de gestion d'urgence. Nous avons une fonction à laquelle nous transférons par exemple une ressource et qui, en théorie, peut provoquer la panique.
func bar(f *os.File) { panic(errors.New("error")) }
Tout d'abord, vous devrez peut-être toujours effectuer certaines actions à la fin, par exemple, nettoyer les ressources, dans notre cas, fermer le fichier.
Deuxièmement, l'exécution incorrecte d'une telle fonction ne doit pas conduire à la fin de l'ensemble du programme.
Ce problème peut être résolu en différant, récupérant et fermant:
func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error)
La fermeture nous permet de nous tourner vers les variables déclarées ci-dessus, grâce à cela nous garantissons de fermer le fichier et en cas d'accident, d'en extraire une erreur et de la transmettre au mécanisme habituel de gestion des erreurs.
Il existe des situations inverses lorsqu'une fonction avec certains arguments doit toujours fonctionner correctement, et si cela ne se produit pas, cela se passe vraiment mal.
Dans de tels cas, ajoutez un wrapper de fonction dans lequel la fonction cible est appelée et en cas d'erreur, la panique est appelée.
Go a généralement des préfixes Must :
Il convient de se rappeler une chose liée à la panique et aux goroutines.
Une partie des thèses de ce qui a été discuté ci-dessus:
- Une pile distincte est allouée pour chaque goroutine.
- Lors de l'appel de panique, la récupération est recherchée sur la pile.
- Dans le cas où la récupération n'est pas trouvée, l'application entière se termine.
Le gestionnaire principal n'interceptera pas la panique de foo et le programme plantera:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) }
Ce sera un problème si, par exemple, un gestionnaire est appelé pour se connecter au serveur. En cas de panique dans l'un des gestionnaires, l'ensemble du serveur terminera l'exécution. Et vous ne pouvez pas contrôler la gestion des accidents dans ces fonctions, pour une raison quelconque.
Dans un cas simple, la solution pourrait ressembler à ceci:
type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) }
poignée / contrôle
Peut-être qu'à l'avenir, nous verrons des changements dans la gestion des erreurs. Vous pouvez les connaître sur les liens:
go2draft
Gestion des erreurs dans Go 2
C'est tout pour aujourd'hui. Je vous remercie!