Fejlhåndtering i Golang

I modsætning til konventionelle metoder i andre almindelige programmeringssprog som JavaScript (der bruger try… catch-erklæringen) eller Python (med try… except-blokken) kræver fejlhåndtering i Go en anden tilgang. Hvorfor? Fordi dets funktioner til fejlhåndtering ofte anvendes forkert.

I dette blogindlæg vil vi se på de bedste fremgangsmåder, der kan bruges til at håndtere fejl i et Go-program. En grundlæggende forståelse af, hvordan Go fungerer, er alt, hvad der kræves for at fordøje denne artikel – skulle du føle dig fastlåst på et tidspunkt, er det okay at tage lidt tid og undersøge ukendte begreber.

Den tomme identifikator

Den tomme identifikator er en anonym placeholder. Den kan bruges som enhver anden identifikator i en deklaration, men den introducerer ikke en binding. Den tomme identifikator giver mulighed for at ignorere venstrehåndede værdier i en tildeling og undgå compilerfejl om ubrugte importer og variabler i et program. Praksis med at tildele fejl til den blanke identifikator i stedet for at håndtere dem korrekt er usikker, da det betyder, at du har besluttet dig for udtrykkeligt at ignorere værdien af den definerede funktion.

result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}

Din begrundelse for sandsynligvis at gøre dette er, at du ikke forventer en fejl fra funktionen (eller hvilken fejl der måtte opstå), men dette kan skabe kaskadevirkninger i dit program. Det bedste er at håndtere en fejl, når du kan.

Håndtering af fejl gennem flere returværdier

En måde at håndtere fejl på er at udnytte det faktum, at funktioner i Go understøtter flere returværdier. Du kan således aflevere en fejlvariabel sammen med resultatet af den funktion, du definerer:

func iterate(x, y int) (int, error) {}

I kodeeksemplet ovenfor skal vi returnere den foruddefinerede error-variabel, hvis vi mener, at der er en chance for, at vores funktion kan mislykkes. error er en grænsefladetype, der er erklæret i Go’s built-in-pakke, og dens nulværdi er nil.

type error interface { Error() string }

Sædvanligvis betyder returnering af en fejl, at der er et problem, og returnering af nil betyder, at der ikke var nogen fejl:

result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }

Så hver gang funktionen iterate kaldes, og err ikke er lig med nil, skal den returnerede fejl håndteres på passende vis – en mulighed kunne være at oprette en instans af en retry- eller oprydningsmekanisme. Den eneste ulempe ved at håndtere fejl på denne måde er, at der ikke er nogen håndhævelse fra Go’s compiler, du skal selv bestemme, hvordan den funktion, du har oprettet, returnerer fejlen. Du kan definere en error struct og placere den i positionen for de returnerede værdier. En måde at gøre dette på er ved at bruge den indbyggede errorString struct (du kan også finde denne kode i Go’s kildekode):

package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }

I kodeeksemplet ovenfor indlejrer errorString en string, som returneres af Error-metoden. For at oprette en brugerdefineret fejl skal du definere din fejlstruct og bruge metodesæt til at knytte en funktion til din struct:

// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}

Den nyligt oprettede brugerdefinerede fejl kan derefter omstruktureres til at bruge den indbyggede error struct:

 import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}

En begrænsning ved den indbyggede error struct er, at den ikke kommer med stack-traces. Dette gør det meget vanskeligt at lokalisere, hvor en fejl opstod. Fejlen kan passere gennem en række funktioner, før den bliver udskrevet. For at håndtere dette kan du installere pkg/errors-pakken, som indeholder grundlæggende fejlhåndteringsprimitiver som f.eks. registrering af stack trace, error wrapping, unwrapping og formatering. For at installere denne pakke skal du køre denne kommando i din terminal:

go get github.com/pkg/errors

Når du har brug for at tilføje stack trace eller andre oplysninger, der gør fejlfinding lettere, til dine fejl, skal du bruge funktionerne New eller Errorf til at levere fejl, der registrerer din stack trace. Errorf implementerer fmt.Formatter-grænsefladen, som giver dig mulighed for at formatere dine fejl ved hjælp af fmt-pakkens runer (%s, %v, %+v osv.):

import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}

For at udskrive stack-traces i stedet for en almindelig fejlmeddelelse skal du bruge %+v i stedet for %v i formatmønsteret, og stack-traces vil se ud som i nedenstående kodeeksempel:

Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Derfer, panik og genopretning

Og selv om Go ikke har undtagelser, har det en lignende slags mekanisme kendt som “Defer, panic, and recover”. Go’s ideologi er, at tilføjelse af undtagelser som f.eks. try/catch/finally-erklæringen i JavaScript ville resultere i kompleks kode og tilskynde programmører til at betegne for mange grundlæggende fejl, f.eks. at det ikke lykkes at åbne en fil, som exceptionelle. Du bør ikke bruge defer/panic/recover som du ville bruge throw/catch/finally; kun i tilfælde af uventede, uoprettelige fejl.

Defer er en sprogmekanisme, der placerer dit funktionskald i en stak. Hver udskudt funktion udføres i omvendt rækkefølge, når værtsfunktionen afsluttes, uanset om der kaldes en panikfunktion eller ej. Defer-mekanismen er meget nyttig til oprydning af ressourcer:

package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}

Dette ville kompilere som:

If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!

Panic er en indbygget funktion, der stopper den normale eksekveringsstrøm. Når du kalder panic i din kode, betyder det, at du har besluttet, at din opkalder ikke kan løse problemet. Derfor bør panic kun bruges i sjældne tilfælde, hvor det ikke er sikkert for din kode eller nogen, der integrerer din kode, at fortsætte på det punkt. Her er et kodeeksempel, der viser, hvordan panic fungerer:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Eksemplet ovenfor ville kompilere som:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.

Som vist ovenfor stopper eksekveringsflowet, når panic bruges og ikke håndteres, stopper eksekveringsflowet, alle udskudte funktioner udføres i omvendt rækkefølge, og der udskrives stack-traces.

Du kan bruge den indbyggede funktion recover til at håndtere panic og returnere de værdier, der overføres fra et panikopkald. recover skal altid kaldes i en defer-funktion, ellers returnerer den nil:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Som det ses i kodeeksemplet ovenfor, forhindrer recover hele eksekveringsflowet i at gå i stå, fordi vi smed en panic-funktion ind, og compileren ville returnere:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.

For at rapportere en fejl som en returværdi skal du kalde recover-funktionen i samme goroutine som panic-funktionen kaldes, hente en fejlstruct fra recover-funktionen og sende den til en variabel:

package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}

Alle udskudte funktioner vil blive udført efter et funktionskald, men før en returangivelse. Du kan altså indstille en returneret variabel, før en returanvisning bliver udført. Ovenstående kodeeksempel ville kompilere som:

If it's more than 100 degrees...Then there's nothing we can doProgram exited.

Fejlindpakning

Fejlindpakning i Go var tidligere kun tilgængelig via brug af pakker som pkg/errors. Med Go’s seneste udgave – version 1.13 – er der imidlertid understøttelse for error wrapping. Ifølge udgivelsesbemærkningerne:

En fejl e kan ombryde en anden fejl w ved at levere en Unwrap metode, der returnerer w. Både e og w er tilgængelige for programmer, hvilket gør det muligt for e at give yderligere kontekst til w eller at omfortolke den, mens programmerne stadig kan træffe beslutninger baseret på w.

For at skabe indpakkede fejl har fmt.Errorf nu et verbum %w, og til at inspicere og udpakke fejl er der blevet tilføjet et par funktioner til error-pakken:

errors.Unwrap: Denne funktion inspicerer og afslører grundlæggende fejl i et program. Den returnerer resultatet af at kalde Unwrap-metoden på Err. Hvis Err’s type indeholder en Unwrap-metode, der returnerer en fejl. Ellers returnerer Unwrap nil.

package errorstype Wrapper interface{ Unwrap() error}

Nedenfor er en eksempelimplementering af Unwrap-metoden:

func(e*PathError)Unwrap()error{ return e.Err}

errors.Is: Med denne funktion kan du sammenligne en fejlværdi med sentinel-værdien. Det, der gør denne funktion anderledes end vores sædvanlige fejlkontroller, er, at den i stedet for at sammenligne sentinelværdien med én fejl sammenligner den med alle fejl i fejlkæden. Den implementerer også en Is-metode på en fejl, så en fejl kan bogføre sig selv som en sentinelværdi, selv om den ikke er en sentinelværdi.

func Is(err, target error) bool

I den grundlæggende implementering ovenfor kontrollerer og rapporterer Is, om err eller en af errors i dens kæde er lig med target (sentinelværdi).

errors.As: Denne funktion giver en måde at kaste til en bestemt fejltype på. Den leder efter den første fejl i fejlkæden, der matcher sentinelværdien, og hvis den findes, indstiller den sentinelværdien til denne fejlværdi og returnerer true:

package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}

Du kan finde denne kode i Go’s kildekode.

Compilerresultat:

Failed at path: non-existingProgram exited.

En fejl matcher sentinelværdien, hvis fejlens konkrete værdi kan tildeles den værdi, der peges på af sentinelværdien. As går i panik, hvis sentinel-værdien ikke er en ikke-nul-pointer til enten en type, der implementerer fejl, eller til en grænsefladetype. As returnerer false, hvis err er nil.

Summary

Go-fællesskabet har gjort imponerende fremskridt på det seneste med understøttelse af forskellige programmeringsbegreber og introduktion af endnu mere præcise og nemme måder at håndtere fejl på. Har du nogle idéer til, hvordan du kan håndtere eller arbejde med fejl, der kan forekomme i dit Go-program? Lad mig endelig høre fra dig i kommentarerne nedenfor.

Ressourcer:
Gos programmeringssprogspecifikation om Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes

LogRocket: Fuld synlighed i dine webapps

LogRocket er en løsning til overvågning af frontend-applikationer, der giver dig mulighed for at afspille problemer, som om de var sket i din egen browser. I stedet for at gætte på, hvorfor fejl opstår, eller bede brugerne om skærmbilleder og log-dumps, kan du med LogRocket afspille sessionen igen for hurtigt at forstå, hvad der gik galt. Det fungerer perfekt med enhver app, uanset framework, og har plugins til at logge yderligere kontekst fra Redux, Vuex og @ngrx/store.

Ud over at logge Redux-handlinger og -tilstand registrerer LogRocket konsollogs, JavaScript-fejl, stacktraces, netværksanmodninger/-svar med headers + bodies, browsermetadata og brugerdefinerede logs. Den instrumenterer også DOM’en for at registrere HTML og CSS på siden og genskaber pixelperfekte videoer af selv de mest komplekse enkeltsidede apps.

Prøv det gratis.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.