Hibakezelés a Golangban
A hagyományos módszerekkel ellentétben más mainstream programozási nyelvekben, például a JavaScriptben (amely a try… catch
utasítást használja) vagy a Pythonban (a try… except
blokkal) a hibák kezelése a Go-ban más megközelítést igényel. Hogy miért? Mert a hibakezelésre szolgáló funkcióit gyakran rosszul alkalmazzák.
Ebben a blogbejegyzésben megnézzük a legjobb gyakorlatokat, amelyeket a hibák kezelésére használhatunk egy Go alkalmazásban. A cikk megemésztéséhez mindössze a Go működésének alapvető megértése szükséges – ha egy ponton elakadna, nyugodtan szánjon rá egy kis időt, és kutasson az ismeretlen fogalmak után.
A blank identifier
A blank identifier egy névtelen helyőrző. Ugyanúgy használható, mint bármely más azonosító a deklarációban, de nem vezet be kötést. Az üres azonosító lehetőséget biztosít arra, hogy a balkezes értékeket egy hozzárendelésben figyelmen kívül hagyjuk, és elkerüljük a fordítói hibákat a programban nem használt importált és nem használt változókkal kapcsolatban. Az a gyakorlat, hogy a hibákat az üres azonosítóhoz rendeljük ahelyett, hogy megfelelően kezelnénk őket, nem biztonságos, mivel ez azt jelenti, hogy úgy döntöttünk, hogy kifejezetten figyelmen kívül hagyjuk a definiált függvény értékét.
result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}
Ezt valószínűleg azért csináljuk, mert nem várunk hibát a függvénytől (vagy bármilyen hibától), de ez kaszkádhatást okozhat a programban. A legjobb, amit tehetsz, hogy kezeled a hibát, amikor csak tudod.
Hibakezelés több visszatérési értéken keresztül
A hibakezelés egyik módja az, hogy kihasználod azt a tényt, hogy a függvények a Go-ban támogatják a több visszatérési értéket. Így a definiált függvény eredménye mellett átadhatunk egy hibaváltozót is:
func iterate(x, y int) (int, error) {}
A fenti kódpéldában az előre definiált error
változót kell visszaadnunk, ha úgy gondoljuk, hogy van rá esély, hogy a függvényünk kudarcot vall. A error
a Go built-in
csomagjában deklarált interfész típus, és a nulla értéke nil
.
type error interface { Error() string }
A hiba visszatérése általában azt jelenti, hogy probléma van, a nil
visszatérése pedig azt, hogy nem volt hiba:
result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }
Amikor tehát a iterate
függvényt meghívjuk és a err
nem egyenlő a nil
értékkel, a visszaadott hibát megfelelően kell kezelni – egy lehetőség lehet, hogy létrehozzuk egy újrapróbálkozási vagy tisztítási mechanizmus példányát. A hibák ilyen módon történő kezelésének egyetlen hátránya, hogy nincs kényszerítés a Go fordítója részéről, neked kell eldöntened, hogy a létrehozott függvény hogyan adja vissza a hibát. Definiálhatsz egy hibastruktúrát, és elhelyezheted a visszaadott értékek pozíciójában. Ennek egyik módja a beépített errorString
struktúra használata (ezt a kódot a Go forráskódjában is megtalálod):
package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }
A fenti kódpéldában a errorString
beágyaz egy string
-et, amelyet a Error
metódus ad vissza. Egyéni hiba létrehozásához definiálnunk kell a hiba struktúránkat, és metódushalmazok segítségével egy függvényt kell társítanunk a struktúránkhoz:
// 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" }}
Az újonnan létrehozott egyéni hibát ezután átstrukturálhatjuk a beépített error
struktúra használatára:
import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}
A beépített error
struktúra egyik korlátja, hogy nem rendelkezik stack traces-szel. Ez nagyon megnehezíti a hiba keletkezési helyének felkutatását. A hiba több függvényen is áthaladhat, mielőtt kiírásra kerülne. Ennek kezelésére telepíthetjük a pkg/errors
csomagot, amely olyan alapvető hibakezelési primitíveket biztosít, mint a stack trace rögzítése, a hibák fel- és kicsomagolása és formázása. A csomag telepítéséhez futtassa a következő parancsot a termináljában:
go get github.com/pkg/errors
Ha a hibáihoz veremnyomokat vagy bármilyen más, a hibakeresést megkönnyítő információt szeretne hozzáadni, használja a New
vagy Errorf
függvényeket, hogy olyan hibákat adjon meg, amelyek rögzítik a veremnyomot. A Errorf
megvalósítja a fmt.Formatter
interfészt, amely lehetővé teszi, hogy a fmt
csomag rúnáinak (%s
, %v
, %+v
stb.) használatával formázd a hibáidat:
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())}
Hogy stack traces-t írj ki egy sima hibaüzenet helyett, a %v
helyett a %+v
-t kell használnod a formázási mintában, és a stack traces az alábbi kódpéldához hasonlóan fog kinézni:
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
A Go ugyan nem rendelkezik kivételekkel, de van egy hasonló jellegű mechanizmusa, amelyet “Defer, panic, and recover” néven ismerünk. A Go ideológiája az, hogy a JavaScriptben a try/catch/finally
utasításhoz hasonló kivételek hozzáadása bonyolult kódot eredményezne, és arra ösztönözné a programozókat, hogy túl sok alapvető hibát, például egy fájl megnyitásának sikertelenségét, kivételesnek címkézzék. A defer/panic/recover
-t nem szabad úgy használni, mint a throw/catch/finally
-t; csak váratlan, helyrehozhatatlan hiba esetén.
A
Defer
egy nyelvi mechanizmus, amely a függvényhívást a verembe helyezi. Minden elhalasztott függvény fordított sorrendben hajtódik végre, amikor a gazdafüggvény befejeződik, függetlenül attól, hogy pánikhívás történt-e vagy sem. A halasztási mechanizmus nagyon hasznos az erőforrások takarítására:
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()}
Ez így fordítható le:
If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!
Panic
egy beépített függvény, amely megállítja a normál végrehajtási folyamot. Ha a panic
-t hívod a kódodban, az azt jelenti, hogy úgy döntöttél, hogy a hívóprogramod nem tudja megoldani a problémát. Ezért a panic
csak olyan ritka esetekben használható, amikor nem biztonságos a kódod vagy a kódodat integráló bárki számára, hogy ezen a ponton folytasd. Íme egy kódminta, amely bemutatja a panic
működését:
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()}
A fenti minta a következőképpen fordítható le:
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.
Amint fentebb látható, ha a panic
-et használjuk és nem kezeljük, a végrehajtási folyam megáll, az összes halasztott függvény fordított sorrendben végrehajtódik, és stack traces kiírásra kerül.
A recover
beépített függvényt használhatjuk a panic
kezelésére és a pánikhívásból átadott értékek visszaadására. A recover
mindig egy defer
függvényben kell meghívni, különben nil
fog visszatérni:
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()}
Amint a fenti kódpéldában látható, a recover
megakadályozza, hogy a teljes végrehajtási folyam megálljon, mert bedobtunk egy panic
függvényt, és a fordító visszatérne:
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.
A hiba visszatérési értékként történő jelentéséhez a recover
függvényt ugyanabban a goroutine-ban kell meghívnunk, amelyben a panic
függvényt is meghívtuk, a recover
függvényből le kell kérnünk egy hibastruktúrát, és át kell adnunk egy változónak:
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)}
Minden elhalasztott függvény a függvényhívás után, de a visszatérési utasítás előtt kerül végrehajtásra. Tehát a return utasítás végrehajtása előtt beállíthat egy visszaadott változót. A fenti kódminta így fordítható:
If it's more than 100 degrees...Then there's nothing we can doProgram exited.
Hibacsomagolás
A hibacsomagolás a Go-ban korábban csak olyan csomagok használatával volt elérhető, mint a pkg/errors
. A Go legújabb kiadásában – az 1.13-as verzióban – azonban már van támogatás a hibatekercselésre. A kiadási megjegyzések szerint:
Egy
e
hibaw
egy másikw
hibát is becsomagolhat egyUnwrap
metódus megadásával, amelyw
-t ad vissza. Mind ae
, mind aw
elérhető a programok számára, lehetővé téve, hogy ae
további kontextust adjon aw
-nak, vagy újraértelmezze azt, miközben a programok továbbra is aw
alapján hozhatnak döntéseket.
A becsomagolt hibák létrehozásához a fmt.Errorf
mostantól rendelkezik egy %w
igével, a hibák vizsgálatához és kicsomagolásához pedig a error
csomag néhány függvénnyel bővült:
errors.Unwrap
: Ez a függvény alapvetően megvizsgálja és feltárja a program mögöttes hibáit. A Err
Unwrap
metódus meghívásának eredményét adja vissza. Ha az Err típusa tartalmaz egy Unwrap
metódust, amely hibát ad vissza. Ellenkező esetben a Unwrap
nil
-et ad vissza.
package errorstype Wrapper interface{ Unwrap() error}
Az alábbiakban egy példa a Unwrap
módszer implementációjára:
func(e*PathError)Unwrap()error{ return e.Err}
errors.Is
: Ezzel a függvénnyel egy hibaértéket hasonlíthatunk össze a sentinel értékkel. Ez a függvény abban különbözik a szokásos hibaellenőrzéseinktől, hogy a sentinel értéket nem egy hibával, hanem a hibalánc minden hibájával összehasonlítja. Emellett egy Is
metódust is megvalósít egy hibán, így egy hiba akkor is feladhatja magát sentinelként, ha nem sentinel érték.
func Is(err, target error) bool
A fenti alap implementációban a Is
ellenőrzi és jelenti, hogy a err
vagy bármelyik errors
a láncában megegyezik-e a target (sentinel érték) értékkel.
errors.As
: Ez a függvény lehetőséget biztosít egy adott hibatípusra való castolásra. Megkeresi a hibaláncban az első olyan hibát, amely megfelel a sentinel-értéknek, és ha megtalálja, akkor a sentinel-értéket erre a hibaértékre állítja, és true
visszatér:
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) } }}
Ezt a kódot a Go forráskódjában találja.
A fordító eredménye:
Failed at path: non-existingProgram exited.
Egy hiba megfelel a sentinel-értéknek, ha a hiba konkrét értéke hozzárendelhető a sentinel-érték által mutatott értékhez. As
Pánikba esik, ha a sentinel érték nem egy nem-nil mutató sem a hibát megvalósító típusra, sem pedig bármelyik interfész típusra. As
false-t ad vissza, ha err
nil
.
Összefoglaló
A Go közösség az utóbbi időben lenyűgöző lépéseket tett a különböző programozási koncepciók támogatásával és a hibakezelés még tömörebb és egyszerűbb módjainak bevezetésével. Van valami ötleted arra, hogyan kezeld vagy dolgozz a Go programodban megjelenő hibákat? Oszd meg velem az alábbi megjegyzésekben.
Források:
Go programozási nyelvi specifikációja a Type assertionról
Marcel van Lohuizen előadása a dotGo 2019-en – Go 2 error values today
Go 1.13 release notes
LogRocket: Teljes rálátás a webes alkalmazásaira
A LogRocket egy frontend alkalmazásfelügyeleti megoldás, amely lehetővé teszi a problémák visszajátszását, mintha azok a saját böngészőjében történtek volna. Ahelyett, hogy találgatná, miért történtek a hibák, vagy képernyőképeket és naplófájlokat kérne a felhasználóktól, a LogRocket lehetővé teszi a munkamenet visszajátszását, hogy gyorsan megértse, mi romlott el. Tökéletesen működik bármilyen alkalmazással, függetlenül a keretrendszertől, és rendelkezik bővítményekkel a Redux, Vuex és @ngrx/store további kontextusának naplózásához.
A Redux-akciók és állapot naplózása mellett a LogRocket rögzíti a konzolnaplókat, JavaScript hibákat, stacktraces-t, hálózati kéréseket/válaszokat fejlécekkel + testekkel, böngésző metaadatokat és egyéni naplókat. A DOM-ot is instrumentálja az oldalon lévő HTML és CSS rögzítéséhez, így még a legösszetettebb egyoldalas alkalmazások pixelpontos videóit is újraalkotja.
Próbálja ki ingyenesen.