Felhantering i Golang
Till skillnad från konventionella metoder i andra vanliga programmeringsspråk som JavaScript (som använder try… catch
-angivelsen) eller Python (med try… except
-blocket) kräver felhantering i Go ett annat tillvägagångssätt. Varför? Därför att dess funktioner för felhantering ofta tillämpas felaktigt.
I det här blogginlägget tar vi en titt på de bästa metoderna som kan användas för att hantera fel i en Go-applikation. En grundläggande förståelse för hur Go fungerar är allt som krävs för att smälta den här artikeln – om du skulle känna dig fast vid något tillfälle är det okej att ta lite tid och undersöka okända begrepp.
Den tomma identifieraren
Den tomma identifieraren är en anonym platshållare. Den kan användas som vilken annan identifierare som helst i en deklaration, men den introducerar ingen bindning. Den tomma identifieraren ger ett sätt att ignorera vänstervärden i en tilldelning och undvika kompilerfel om oanvända importer och variabler i ett program. Att tilldela fel till den tomma identifieraren i stället för att hantera dem på rätt sätt är osäkert eftersom det innebär att du har bestämt dig för att uttryckligen ignorera värdet av den definierade funktionen.
result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}
Din anledning till att du troligen gör detta är att du inte förväntar dig ett fel från funktionen (eller vilket fel som helst), men detta kan skapa kaskad-effekter i ditt program. Det bästa är att hantera ett fel när du kan.
Hantering av fel genom flera returvärden
Ett sätt att hantera fel är att dra nytta av att funktioner i Go stöder flera returvärden. På så sätt kan du skicka en felvariabel tillsammans med resultatet av den funktion du definierar:
func iterate(x, y int) (int, error) {}
I kodexemplet ovan måste vi returnera den fördefinierade variabeln error
om vi tror att det finns en chans att vår funktion kan misslyckas. error
är en gränssnittstyp som deklareras i Go:s built-in
-paket och dess nollvärde är nil
.
type error interface { Error() string }
Ovanligtvis betyder att returnera ett fel att det finns ett problem och att returnera nil
betyder att det inte fanns några fel:
result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }
Så närhelst funktionen iterate
anropas och err
inte är lika med nil
ska det returnerade felet hanteras på lämpligt sätt – ett alternativ skulle kunna vara att skapa en instans av en omprövnings- eller rensningsmekanism. Den enda nackdelen med att hantera fel på det här sättet är att det inte finns någon verkställighet från Go:s kompilator, du måste själv bestämma hur den funktion du skapat returnerar felet. Du kan definiera en felstruct och placera den i positionen för de returnerade värdena. Ett sätt att göra detta är att använda den inbyggda strukturen errorString
(du kan också hitta den här koden i Go:s källkod):
package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }
I kodexemplet ovan bäddar errorString
in en string
som returneras av metoden Error
. För att skapa ett anpassat fel måste du definiera din felstruktur och använda metoduppsättningar för att associera en funktion till din struktur:
// 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" }}
Det nyskapade anpassade felet kan sedan omstruktureras för att använda den inbyggda error
strukturen:
import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}
En begränsning för den inbyggda error
strukturen är att den inte kommer med stacktraces. Detta gör det mycket svårt att lokalisera var ett fel inträffade. Felet kan passera genom ett antal funktioner innan det skrivs ut. För att hantera detta kan du installera pkg/errors
-paketet som tillhandahåller grundläggande felhanteringsmoment, t.ex. registrering av stackspår, inplastning, utplastning och formatering av fel. För att installera det här paketet kör du det här kommandot i din terminal:
go get github.com/pkg/errors
När du behöver lägga till stackspår eller annan information som underlättar felsökning till dina fel, använd funktionerna New
eller Errorf
för att tillhandahålla fel som registrerar ditt stackspår. Errorf
implementerar gränssnittet fmt.Formatter
som låter dig formatera dina fel med hjälp av fmt
-paketets runor (%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())}
För att skriva ut stackspår istället för ett vanligt felmeddelande måste du använda %+v
istället för %v
i formatmönstret, och stackspåren kommer att se ut ungefär som i kodprovet nedan:
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
Trots att Go inte har undantag har det en liknande typ av mekanism som kallas ”Defer, panic, and recover”. Go:s ideologi är att om man lägger till undantag som try/catch/finally
-anvisningen i JavaScript skulle det resultera i komplex kod och uppmuntra programmerare att beteckna alltför många grundläggande fel, t.ex. att misslyckas med att öppna en fil, som exceptionella. Du bör inte använda defer/panic/recover
som du skulle använda throw/catch/finally
; endast i fall av oväntade, oåterkalleliga fel.
Defer
är en språkmekanism som placerar ditt funktionsanrop i en stapel. Varje uppskjuten funktion exekveras i omvänd ordning när värdfunktionen avslutas, oavsett om en panikfunktion kallas eller inte. Uppskjutningsmekanismen är mycket användbar för att rensa upp resurser:
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()}
Detta skulle kompileras som:
If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!
Panic
är en inbyggd funktion som stoppar det normala exekveringsflödet. När du anropar panic
i din kod betyder det att du har bestämt dig för att din anropare inte kan lösa problemet. Därför bör panic
endast användas i sällsynta fall där det inte är säkert för din kod eller någon som integrerar din kod att fortsätta på den punkten. Här är ett kodprov som visar hur panic
fungerar:
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()}
Exemplet ovan skulle kompileras 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 framgår ovan, när panic
används och inte hanteras, stannar exekveringsflödet, alla uppskjutna funktioner exekveras i omvänd ordning och stapelspår skrivs ut.
Du kan använda den inbyggda funktionen recover
för att hantera panic
och returnera de värden som passerar från ett panikanrop. recover
måste alltid anropas i en defer
funktion annars returneras 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 framgår av kodexemplet ovan förhindrar recover
att hela exekveringsflödet stannar upp eftersom vi slängde in en panic
funktion och kompilatorn skulle returnera:
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.
För att rapportera ett fel som ett returvärde måste du anropa recover
-funktionen i samma goroutin som panic
-funktionen anropas, hämta en error struct från recover
-funktionen och skicka den till 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)}
Varje uppskjuten funktion kommer att exekveras efter ett funktionsanrop men före ett returmeddelande. Du kan alltså ställa in en returnerad variabel innan ett return statement exekveras. Kodprovet ovan skulle kompileras som:
If it's more than 100 degrees...Then there's nothing we can doProgram exited.
Felomslag
Förut var felomslag i Go endast tillgängligt genom att använda paket som pkg/errors
. Men med Go:s senaste version – version 1.13 – finns det stöd för felomslag. Enligt versionsanteckningarna:
Ett fel
e
kan omsluta ett annat felw
genom att tillhandahålla enUnwrap
metod som returnerarw
. Bådee
ochw
är tillgängliga för program, vilket gör det möjligt före
att ge ytterligare kontext tillw
eller att omtolka den samtidigt som program fortfarande kan fatta beslut baserat påw
.
För att skapa inplastade fel har fmt.Errorf
nu ett verb %w
och för att inspektera och avveckla fel har ett par funktioner lagts till i error
-paketet:
errors.Unwrap
: Den här funktionen inspekterar och avslöjar i princip de underliggande felen i ett program. Den returnerar resultatet av att anropa metoden Unwrap
på Err
. Om Errs typ innehåller en Unwrap
-metod som returnerar ett fel. Annars returnerar Unwrap
nil
.
package errorstype Wrapper interface{ Unwrap() error}
Nedan följer en exempelimplementation av Unwrap
-metoden:
func(e*PathError)Unwrap()error{ return e.Err}
errors.Is
: Med den här funktionen kan du jämföra ett felvärde med ett sentinelvärde. Det som skiljer den här funktionen från våra vanliga felkontroller är att den istället för att jämföra sentinelvärdet med ett fel jämför den det med varje fel i felkedjan. Den implementerar också en Is
-metod på ett fel så att ett fel kan lägga upp sig självt som en sentinel även om det inte är ett sentinelvärde.
func Is(err, target error) bool
I den grundläggande implementeringen ovan kontrollerar Is
och rapporterar om err
eller någon av errors
i dess kedja är lika med target (sentinelvärdet).
errors.As
: Den här funktionen tillhandahåller ett sätt att casta till en specifik feltyp. Den letar efter det första felet i felkedjan som matchar sentinelvärdet och om den hittas, sätter den sentinelvärdet till det felvärdet och returnerar 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) } }}
Den här koden finns i Go:s källkod.
Compilerresultat:
Failed at path: non-existingProgram exited.
Ett fel matchar sentinelvärdet om felets konkreta värde kan tilldelas det värde som sentinelvärdet pekar på. As
får panik om sentinelvärdet inte är en icke-nil pekare till antingen en typ som implementerar fel eller till någon gränssnittstyp. As
returnerar false om err
är nil
.
Sammanfattning
Go-gemenskapen har gjort imponerande framsteg på senare tid med stöd för olika programmeringskoncept och introducerat ännu mer koncisa och enkla sätt att hantera fel. Har du några idéer om hur du kan hantera eller arbeta med fel som kan dyka upp i ditt Go-program? Låt mig veta i kommentarerna nedan.
Resurser:
Gos programmeringsspråksspecifikation om Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes
LogRocket: Full insyn i dina webbapplikationer
LogRocket är en lösning för övervakning av frontend-applikationer som låter dig spela upp problem som om de hände i din egen webbläsare. Istället för att gissa varför fel uppstår eller be användarna om skärmdumpar och loggdumpningar kan du med LogRocket spela upp sessionen igen för att snabbt förstå vad som gick fel. Den fungerar perfekt med alla appar, oavsett ramverk, och har plugins för att logga ytterligare kontext från Redux, Vuex och @ngrx/store.
Förutom att logga Redux-åtgärder och -tillstånd registrerar LogRocket konsolloggar, JavaScript-fel, stacktraces, nätverksförfrågningar/-svar med rubriker + kroppar, webbläsarmetadata och anpassade loggar. Den instrumenterar även DOM för att registrera HTML och CSS på sidan och återskapar pixel-perfekta videor av även de mest komplexa appar med en enda sida.
Prova det gratis.