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 hiba w egy másik w hibát is becsomagolhat egy Unwrap metódus megadásával, amely w-t ad vissza. Mind a e, mind a w elérhető a programok számára, lehetővé téve, hogy a e további kontextust adjon a w-nak, vagy újraértelmezze azt, miközben a programok továbbra is a w 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.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.