Matériel technique
Les programmes Go utilisent os.Exit
ou log.Fatal*
pour sortir immédiatement (utiliser panic
n'est pas une bonne façon de sortir du programme, veuillez ne pas utiliser panic).
Appelez uniquement l'une des fonctions os.Exit
ou log.Fatal*
dans main()
. Toutes les autres fonctions doivent renvoyer des erreurs à l'appelant.
Non recommandé:
func main() {
body := readFile(path)
fmt.Println(body)
}
func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
}
Recommandé:
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
b, err := os.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
En principe, les programmes avec plusieurs points de sortie présentent plusieurs problèmes :
- Flux de contrôle obscur : n'importe quelle fonction peut terminer le programme, ce qui rend difficile de raisonner sur le flux de contrôle.
- Difficulté de test : les fonctions qui terminent le programme terminent également les tests qui les appellent. Cela rend les fonctions difficiles à tester et augmente le risque de sauter d'autres tests non encore exécutés par
go test
. - Saut de nettoyage : lorsqu'une fonction termine le programme, elle saute les appels de fonction différés. Cela augmente le risque de sauter des tâches de nettoyage importantes.
Sortie unique
Dans la mesure du possible, il ne doit y avoir qu'un maximum de un appel à os.Exit
ou log.Fatal
dans votre fonction main()
. Si plusieurs scénarios d'erreur nécessitent d'arrêter l'exécution du programme, placez cette logique dans une fonction séparée et renvoyez l'erreur de là. Cela raccourcira la fonction main()
et mettra toute la logique métier critique dans une fonction séparée et testable.
Approche non recommandée :
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("fichier manquant")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Si nous appelons log.Fatal après cette ligne
// f.Close sera exécuté.
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
Approche recommandée :
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("fichier manquant")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := os.ReadAll(f)
// ...
if err != nil {
return err
}
}
L'exemple ci-dessus utilise log.Fatal
, mais ce principe s'applique également à os.Exit
ou à tout code de bibliothèque qui appelle os.Exit
.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Vous pouvez modifier la signature de run()
selon vos besoins. Par exemple, si votre programme doit se terminer avec un code d'erreur spécifique, run()
peut renvoyer le code de sortie au lieu d'une erreur. Cela permet également aux tests unitaires de valider directement ce comportement.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
Veuillez noter que la fonction run()
utilisée dans ces exemples n'est pas obligatoire. Le nom, la signature et la configuration de la fonction run()
sont flexibles. Entre autres, vous pouvez :
- Accepter des arguments de ligne de commande non analysés (par exemple,
run(os.Args[1:])
) - Analyser les arguments de la ligne de commande dans
main()
et les passer àrun
- Renvoyer le code de sortie à
main()
avec un type d'erreur personnalisé - Mettre la logique métier à un niveau d'abstraction différent
package main
Ce principe exige seulement qu'il y ait un endroit dans main()
responsable du flux de sortie réel.