Foutafhandeling in Golang

In tegenstelling tot conventionele methodes in andere gangbare programmeertalen zoals JavaScript (dat het try… catch statement gebruikt) of Python (met zijn try… except blok) vereist het aanpakken van fouten in Go een andere aanpak. Waarom? Omdat de functies voor foutafhandeling vaak verkeerd worden toegepast.

In deze blogpost kijken we naar de best practices die kunnen worden gebruikt om fouten af te handelen in een Go-toepassing. Een basiskennis van hoe Go werkt is alles wat nodig is om dit artikel te verteren – mocht u zich op een bepaald punt vast voelen zitten, dan is het goed om wat tijd te nemen en onbekende concepten te onderzoeken.

De blanco identifier

De blanco identifier is een anonieme plaatshouder. Hij kan net als elke andere identifier in een declaratie worden gebruikt, maar hij introduceert geen binding. De blank identifier biedt een manier om linksdraaiende waarden in een toewijzing te negeren en compilerfouten over ongebruikte imports en variabelen in een programma te vermijden. De praktijk van het toewijzen van fouten aan de lege identifier in plaats van ze op de juiste manier af te handelen is onveilig, omdat dit betekent dat u hebt besloten om de waarde van de gedefinieerde functie expliciet te negeren.

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

De reden waarom u dit waarschijnlijk doet, is dat u geen fout van de functie verwacht (of welke fout dan ook kan optreden), maar dit zou cascade-effecten in uw programma kunnen veroorzaken. Het beste is om een fout af te handelen wanneer u dat kunt.

Fouten afhandelen via meervoudige retourwaarden

Een manier om fouten af te handelen is door gebruik te maken van het feit dat functies in Go meervoudige retourwaarden ondersteunen. Zo kunt u een foutvariabele doorgeven naast het resultaat van de functie die u definieert:

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

In het bovenstaande codevoorbeeld moeten we de vooraf gedefinieerde variabele error teruggeven als we denken dat er een kans is dat onze functie mislukt. error is een interface type gedeclareerd in Go’s built-in pakket en zijn nulwaarde is nil.

type error interface { Error() string }

Normaal gesproken betekent het retourneren van een fout dat er een probleem is en het retourneren van nil betekent dat er geen fouten waren:

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

Dus telkens als de functie iterate wordt aangeroepen en err is niet gelijk aan nil, moet de geretourneerde fout op de juiste manier worden afgehandeld – een optie zou kunnen zijn om een instantie van een retry- of cleanup-mechanisme te maken. Het enige nadeel van het op deze manier afhandelen van fouten is dat er geen handhaving is door Go’s compiler, u moet zelf beslissen hoe de functie die u heeft gemaakt de fout teruggeeft. Je kunt een error struct definiëren en deze plaatsen op de positie van de geretourneerde waarden. Een manier om dit te doen is door gebruik te maken van de ingebouwde errorString struct (u kunt deze code ook vinden in Go’s broncode):

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

In het codevoorbeeld hierboven, omsluit errorString een string die wordt geretourneerd door de Error methode. Om een aangepaste fout te maken, moet u uw fout struct definiëren en method sets gebruiken om een functie aan uw struct te koppelen:

// 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" }}

De nieuw gemaakte aangepaste fout kan dan worden geherstructureerd om de ingebouwde error struct te gebruiken:

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

Eén beperking van de ingebouwde error struct is dat het niet wordt geleverd met stack traces. Dit maakt het lokaliseren waar een fout is opgetreden erg moeilijk. De fout kan door een aantal functies gaan voordat hij wordt afgedrukt. Om hiermee om te gaan, zou u het pkg/errors pakket kunnen installeren, dat basisprimitieven voor foutafhandeling biedt, zoals het opnemen van stack traces, error wrapping, unwrapping, en formattering. Om dit pakket te installeren, voert u dit commando uit in uw terminal:

go get github.com/pkg/errors

Wanneer u stack traces of andere informatie die het debuggen vergemakkelijkt aan uw fouten moet toevoegen, gebruikt u de New of Errorf functies om fouten te leveren die uw stack trace opnemen. Errorf implementeert de fmt.Formatter interface waarmee u uw fouten kunt formatteren met behulp van de fmt package runes (%s, %v, %+v etc):

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())}

Om stack traces af te drukken in plaats van een gewone foutmelding, moet u %+v gebruiken in plaats van %v in het format pattern, en de stack traces zullen er ongeveer uitzien als in het code voorbeeld hieronder:

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

Defer, panic, and recover

Hoewel Go geen exceptions heeft, heeft het een gelijkaardig soort mechanisme dat bekend staat als “Defer, panic, and recover”. De ideologie van Go is dat het toevoegen van excepties zoals de try/catch/finally verklaring in JavaScript zou resulteren in complexe code en programmeurs zou aanmoedigen om te veel basisfouten, zoals het niet openen van een bestand, als exceptioneel te bestempelen. U moet defer/panic/recover niet gebruiken zoals u throw/catch/finally zou gebruiken; alleen in gevallen van onverwachte, onherstelbare fouten.

Defer is een taalmechanisme dat uw functieaanroep in een stack plaatst. Elke uitgestelde functie wordt in omgekeerde volgorde uitgevoerd wanneer de host-functie eindigt, ongeacht of er een panic wordt aangeroepen of niet. Het uitstelmechanisme is erg handig voor het opruimen van bronnen:

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()}

Dit zou compileren als:

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

Panic is een ingebouwde functie die de normale uitvoeringsstroom stopt. Wanneer u panic aanroept in uw code, betekent dit dat u hebt besloten dat uw aanroeper het probleem niet kan oplossen. panic moet dus alleen worden gebruikt in zeldzame gevallen waarin het niet veilig is voor uw code of iemand die uw code integreert om op dat punt door te gaan. Hier is een codevoorbeeld dat laat zien hoe panic werkt:

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()}

Het bovenstaande voorbeeld zou compileren als:

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.

Zoals hierboven te zien is, wanneer panic wordt gebruikt en niet wordt afgehandeld, stopt de uitvoeringsstroom, worden alle uitgestelde functies in omgekeerde volgorde uitgevoerd en worden stacksporen afgedrukt.

U kunt de recover ingebouwde functie gebruiken om panic af te handelen en de waarden terug te geven die bij een paniekaanroep worden doorgegeven. recover moet altijd worden aangeroepen in een defer functie anders zal het nil retourneren:

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()}

Zoals te zien is in het codevoorbeeld hierboven, voorkomt recover dat de hele uitvoeringsstroom tot stilstand komt omdat we in een panic functie gooiden en de compiler zou retourneren:

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.

Om een fout als retourwaarde te rapporteren, moet u de recover-functie in dezelfde goroutine aanroepen als de panic-functie wordt aangeroepen, een foutstructuur uit de recover-functie ophalen, en deze aan een variabele doorgeven:

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)}

Elke uitgestelde functie wordt uitgevoerd na een functie-aanroep, maar vóór een return-statement. U kunt dus een geretourneerde variabele instellen voordat een return-instructie wordt uitgevoerd. Het bovenstaande code voorbeeld zou compileren als:

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

Error wrapping

Vorige error wrapping in Go was alleen toegankelijk via het gebruik van packages zoals pkg/errors. Echter, met de laatste release van Go – versie 1.13, is ondersteuning voor error wrapping aanwezig. Volgens de release notes:

Een fout e kan een andere fout w wrappen door een Unwrap-methode te bieden die w retourneert. Zowel e als w zijn beschikbaar voor programma’s, zodat e extra context kan bieden voor w of deze kan herinterpreteren terwijl programma’s nog steeds beslissingen kunnen nemen op basis van w.

Om ingepakte fouten te maken, heeft fmt.Errorf nu een %w werkwoord en voor het inspecteren en uitpakken van fouten zijn een paar functies toegevoegd aan het error pakket:

errors.Unwrap: Deze functie inspecteert in principe de onderliggende fouten in een programma en legt deze bloot. Zij retourneert het resultaat van het aanroepen van de methode Unwrap op Err. Als het type Err een Unwrap-methode bevat die een fout teruggeeft. Anders retourneert Unwrap nil.

package errorstype Wrapper interface{ Unwrap() error}

Hieronder vindt u een voorbeeldimplementatie van de methode Unwrap:

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

errors.Is: Met deze functie kunt u een foutwaarde vergelijken met de sentinelwaarde. Wat deze functie anders maakt dan onze gebruikelijke foutcontroles, is dat in plaats van de sentinelwaarde te vergelijken met één fout, hij deze vergelijkt met elke fout in de foutketen. Het implementeert ook een Is methode op een fout, zodat een fout zichzelf als een sentinel kan plaatsen, ook al is het geen sentinel waarde.

func Is(err, target error) bool

In de basis implementatie hierboven, Is controleert en rapporteert of err of een van de errors in zijn keten gelijk zijn aan doel (sentinel waarde).

errors.As: Deze functie biedt een manier om te casten naar een specifiek fout type. Het zoekt naar de eerste fout in de foutketen die overeenkomt met de sentinelwaarde en indien gevonden, stelt de sentinelwaarde in op die foutwaarde en retourneert 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) } }}

U kunt deze code vinden in de broncode van Go.

Compilerresultaat:

Failed at path: non-existingProgram exited.

Een fout komt overeen met de sentinelwaarde als de concrete waarde van de fout kan worden toegewezen aan de waarde waarnaar de sentinelwaarde verwijst. As zal in paniek raken als de sentinel waarde geen niet-nul pointer is naar ofwel een type dat error implementeert of naar een interface type. As retourneert false als err nil is.

Samenvatting

De Go-gemeenschap heeft de laatste tijd indrukwekkende vooruitgang geboekt met ondersteuning voor verschillende programmeerconcepten en de introductie van nog meer beknopte en eenvoudige manieren om fouten af te handelen. Heb jij ideeën over hoe om te gaan met fouten die kunnen verschijnen in je Go programma? Laat het me weten in de reacties hieronder.

Bronnen:
Go’s programmeertaal specificatie over Type assertion
Marcel van Lohuizen’s talk op dotGo 2019 – Go 2 foutwaarden vandaag
Go 1.13 release notes

LogRocket: Volledig inzicht in uw webapps

LogRocket is een frontend applicatiemonitoringoplossing waarmee u problemen kunt naspelen alsof ze in uw eigen browser zijn gebeurd. In plaats van te gissen naar de oorzaak van fouten of gebruikers te vragen om screenshots en logboekdumps, kunt u met LogRocket de sessie opnieuw afspelen om snel te begrijpen wat er mis is gegaan. Het werkt perfect met elke app, ongeacht het framework, en heeft plugins om extra context van Redux, Vuex, en @ngrx/store.

Naast het loggen van Redux acties en staat, LogRocket registreert console logs, JavaScript fouten, stacktraces, netwerk verzoeken / antwoorden met headers + bodies, browser metadata, en aangepaste logs. Het instrumenteert ook de DOM om de HTML en CSS op de pagina vast te leggen, waardoor zelfs de meest complexe apps met één pagina tot op de pixel nauwkeurig kunnen worden nagemaakt.

Probeer het gratis uit.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.