Virheiden käsittely Golangissa

Toisin kuin perinteiset menetelmät muissa valtavirtaisissa ohjelmointikielissä, kuten JavaScriptissä (jossa käytetään try… catch-lauseketta) tai Pythonissa (jossa käytetään try… except-lohkoa), virheiden käsittely Go-kielessä vaatii erilaisen lähestymistavan. Miksi? Koska sen virheenkäsittelyyn tarkoitettuja ominaisuuksia sovelletaan usein väärin.

Tässä blogikirjoituksessa tarkastelemme parhaita käytäntöjä, joita voitaisiin käyttää virheiden käsittelyyn Go-sovelluksessa. Tämän artikkelin sulatteluun riittää perusymmärrys Go:n toiminnasta – jos tunnet jossakin vaiheessa olevasi jumissa, voit ottaa aikaa ja tutkia tuntemattomia käsitteitä.

Tyhjä tunniste

Tyhjä tunniste on anonyymi paikanvaraaja. Sitä voidaan käyttää kuten mitä tahansa muuta tunnusta julistuksessa, mutta se ei esitä sitovuutta. Tyhjä tunniste tarjoaa tavan jättää huomioimatta vasemmanpuoleiset arvot osoituksessa ja välttää kääntäjän virheet käyttämättömistä tuonnista ja muuttujista ohjelmassa. Käytäntö, jossa virheet osoitetaan tyhjään tunnisteeseen sen sijaan, että ne käsiteltäisiin kunnolla, ei ole turvallinen, koska tämä tarkoittaa, että olet päättänyt nimenomaisesti jättää huomiotta määritellyn funktion arvon.

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

Syy tähän on luultavasti se, että et odota virheitä funktiolta (tai mitä tahansa virhettä voi tapahtua), mutta tämä voi aiheuttaa kaskadivaikutuksia ohjelmassasi. Parasta on käsitellä virhe aina kun se on mahdollista.

Virheiden käsittely useiden paluuarvojen avulla

Yksi tapa käsitellä virheitä on hyödyntää sitä, että Go:n funktiot tukevat useita paluuarvoja. Näin voit välittää virhemuuttujan määrittelemäsi funktion tuloksen mukana:

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

Yllä olevassa koodiesimerkissä joudumme palauttamaan ennalta määritellyn error-muuttujan, jos uskomme, että funktiomme saattaa epäonnistua. error on Go:n built-in-paketissa julistettu rajapintatyyppi ja sen nolla-arvo on nil.

type error interface { Error() string }

Käytännössä virheen palauttaminen tarkoittaa, että ongelma on olemassa ja nil:n palauttaminen tarkoittaa, että virheitä ei ollut:

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

Siten aina kun funktiota iterate kutsutaan ja err ei ole yhtä suuri kuin nil, palautettua virhettä pitäisi käsitellä asianmukaisesti – yksi vaihtoehto voisi olla luoda instanssi uudelleenkokeilu- tai puhdistusmekanismista. Ainoa haitta virheiden käsittelyssä tällä tavalla on se, että Go:n kääntäjä ei pakota, vaan sinun on itse päätettävä, miten luotu funktio palauttaa virheen. Voit määritellä virhe-rakenteen ja sijoittaa sen palautettujen arvojen paikalle. Yksi tapa tehdä tämä on käyttää sisäänrakennettua errorString-rakennetta (löydät tämän koodin myös Go:n lähdekoodista):

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

Yllä olevassa koodiesimerkissä errorString upottaa string:n, jonka metodi Error palauttaa. Luodaksesi mukautetun virheen, sinun täytyy määritellä virhe-rakenne ja käyttää metodisarjoja liittääksesi funktion rakenteeseesi:

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

Uuden luodun mukautetun virheen voi sitten jäsentää uudelleen käyttämään sisäänrakennettua error-rakennetta:

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

Ensimmäinen rajoitus sisäänrakennetussa error-rakenteessa on se, että siinä ei ole pinojälkiä. Tämä tekee virheen tapahtumapaikan paikantamisen hyvin vaikeaksi. Virhe voi kulkea useiden funktioiden läpi ennen kuin se tulostuu. Tämän käsittelemiseksi voit asentaa pkg/errors-paketin, joka tarjoaa perustason virheenkäsittelyn alkeet, kuten pinojäljen tallentamisen, virheiden käärimisen, purkamisen ja muotoilun. Voit asentaa tämän paketin suorittamalla tämän komennon terminaalissasi:

go get github.com/pkg/errors

Kun haluat lisätä virheisiisi pinojälkiä tai muita virheenkorjausta helpottavia tietoja, käytä New– tai Errorf-funktioita tarjotaksesi virheitä, jotka tallentavat pinojäljen. Errorf toteuttaa fmt.Formatter-rajapinnan, jonka avulla voit muotoilla virheesi käyttäen fmt-paketin riimuja (%s, %v, %+v jne.):

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

Tulostaaksesi pinojälkiä pelkän virheilmoituksen sijasta sinun täytyy käyttää %v:n sijasta %+v:tä muotoilumallissa, jolloin pinojäljet näyttävät samankaltaisilta kuin alla olevassa koodin esimerkissä:

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

Vaikka Go:ssa ei ole poikkeuksia, siinä on samantyyppinen mekanismi, joka tunnetaan nimellä ”Defer, panic, and recover”. Go:n ideologia on, että JavaScriptin try/catch/finally-lausekkeen kaltaisten poikkeusten lisääminen johtaisi monimutkaiseen koodiin ja rohkaisisi ohjelmoijia merkitsemään liian monet perusvirheet, kuten tiedoston avaamisen epäonnistumisen, poikkeuksellisiksi. defer/panic/recover:aa ei kannata käyttää kuten throw/catch/finally:aa; vain odottamattomissa, korjaamattomissa vikatapauksissa.

Defer on kielen mekanismi, joka sijoittaa funktiokutsun pinoon. Jokainen lykätty funktio suoritetaan käänteisessä järjestyksessä, kun isäntäfunktio päättyy, riippumatta siitä, kutsutaanko paniikkia vai ei. Lykkäysmekanismi on erittäin hyödyllinen resurssien siivoamisessa:

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

Tämä kääntyisi seuraavasti:

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

Panic on sisäänrakennettu funktio, joka pysäyttää normaalin suoritusvirran. Kun kutsut panic koodissasi, se tarkoittaa, että olet päättänyt, että kutsujasi ei voi ratkaista ongelmaa. Näin ollen panic tulisi käyttää vain harvinaisissa tapauksissa, joissa koodisi tai kenenkään koodia integroivan henkilön ei ole turvallista jatkaa kyseisessä kohdassa. Tässä on koodinäyte, joka kuvaa, miten panic toimii:

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

Ylläoleva esimerkki kääntyisi seuraavasti:

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.

Kuten yllä näkyy, kun panic:tä käytetään eikä sitä käsitellä, suoritusvirta pysähtyy, kaikki lykätyt funktiot suoritetaan käänteisessä järjestyksessä ja pinojäljet tulostetaan.

Voit käyttää sisäänrakennettua funktiota recover käsitellessäsi panic ja palauttaessasi paniikkikutsun kulkemat välitysarvot. recover on aina kutsuttava defer-funktiossa, muuten se palauttaa 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()}

Kuten yllä olevasta koodiesimerkistä näkyy, recover estää koko suoritusvirran pysähtymisen, koska heitimme panic-funktion sisään ja kääntäjä palaisi:

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.

Virheen ilmoittaminen paluuarvona edellyttää, että funktiota recover kutsutaan samassa goroutiinissa, jossa funktiota panic kutsutaan, haetaan funktiosta recover virhestruktuuri ja siirretään se muuttujaan:

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

Jokainen lykätty funktio suoritetaan funktiokutsun jälkeen, mutta ennen paluu-lausetta. Voit siis asettaa palautetun muuttujan ennen kuin return-lause suoritetaan. Yllä oleva koodinäyte kääntyisi seuraavasti:

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

Virheiden kääriminen

Varemmin virheiden käärimiseen Go:ssa pääsi käsiksi vain käyttämällä paketteja kuten pkg/errors. Go:n uusimmassa versiossa 1.13 on kuitenkin tuki virheiden käärimiselle. Julkaisutietojen mukaan:

Virhe e voi kietoa toisen virheen w tarjoamalla Unwrap-metodin, joka palauttaa w. Sekä e että w ovat ohjelmien käytettävissä, jolloin e voi antaa lisäkontekstin w:lle tai tulkita sitä uudelleen, mutta ohjelmat voivat silti tehdä päätöksiä w:n perusteella.

Käärittyjen virheiden luomiseksi fmt.Errorf:ssa on nyt %w-verbi, ja virheiden tarkastamista ja purkamista varten error-pakettiin on lisätty pari funktiota:

errors.Unwrap: Tämä funktio periaatteessa tarkastaa ja paljastaa ohjelman taustalla olevat virheet. Se palauttaa Err-metodin Unwrap kutsumisen tuloksen. Jos Errin tyyppi sisältää Unwrap-metodin, joka palauttaa virheen. Muussa tapauksessa Unwrap palauttaa nil.

package errorstype Wrapper interface{ Unwrap() error}

Alla on esimerkkitoteutus Unwrap-metodista:

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

errors.Is: Tällä funktiolla voit verrata virhearvoa sentinel-arvoon. Tämä funktio eroaa tavallisista virhetarkastuksistamme siinä, että sen sijaan, että se vertaisi sentinel-arvoa yhteen virheeseen, se vertaa sitä virheketjun jokaiseen virheeseen. Se myös toteuttaa Is-metodin virheelle, jotta virhe voi lähettää itsensä sentineliksi, vaikka se ei ole sentinel-arvo.

func Is(err, target error) bool

Yllä olevassa perustoteutuksessa Is tarkistaa ja raportoi, onko err tai jokin errors sen ketjussa oleva errors yhtä suuri kuin target (sentinel-arvo).

errors.As: Tämä funktio tarjoaa tavan castata tiettyyn virhetyyppiin. Se etsii virheketjun ensimmäistä virhettä, joka vastaa sentinel-arvoa, ja jos se löytyy, asettaa sentinel-arvon kyseiseen virhearvoon ja palauttaa 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) } }}

Tämän koodin löydät Go:n lähdekoodista.

Kompilaattorin tulos:

Failed at path: non-existingProgram exited.

Virhe sopii sentinel-arvoon, jos virheen konkreettinen arvo on määritettävissä arvoon, johon sentinel-arvo viittaa. As joutuu paniikkiin, jos sentinel-arvo ei ole ei-nil -osoitin joko tyyppiin, joka toteuttaa virheen, tai johonkin rajapintatyyppiin. As palauttaa false, jos err on nil.

Yhteenveto

Go-yhteisö on tehnyt viime aikoina vaikuttavia edistysaskeleita tukemalla erilaisia ohjelmointikonsepteja ja ottamalla käyttöön entistä ytimekkäämpiä ja helppoja tapoja käsitellä virheitä. Onko sinulla ideoita, miten käsitellä tai työstää Go -ohjelmassasi mahdollisesti esiintyviä virheitä? Kerro minulle alla olevissa kommenteissa.

Lähteet:
Gon ohjelmointikielen spesifikaatio Type assertionista
Marcel van Lohuizenin puheenvuoro dotGo 2019 – Go 2 -virhearvot tänään
Go 1.13 -julkaisun muistiinpanot

LogRocket: Täysi näkyvyys verkkosovelluksiisi

LogRocket on frontend-sovellusten valvontaratkaisu, jonka avulla voit toistaa ongelmat ikään kuin ne tapahtuisivat omassa selaimessasi. Sen sijaan, että arvailisit, miksi virheitä tapahtuu, tai pyytäisit käyttäjiltä kuvakaappauksia ja lokitallenteita, LogRocketin avulla voit toistaa istunnon ja ymmärtää nopeasti, mikä meni pieleen. Se toimii täydellisesti minkä tahansa sovelluksen kanssa kehyksestä riippumatta, ja siinä on liitännäisiä, joilla voi lokata Reduxin, Vuexin ja @ngrx/store:n lisäkontekstin.

Reduxin toimintojen ja tilan lokitiedostojen lisäksi LogRocket tallentaa konsolilokeja, JavaScript-virheitä, stacktracesia, verkkopyyntöjä/vastauksia otsikoineen + runkoineen, selaimen metatietoja ja mukautettuja lokeja. Se myös instrumentoi DOM:n tallentaakseen sivun HTML:n ja CSS:n, mikä luo pikselitarkkoja videoita jopa kaikkein monimutkaisimmista yhden sivun sovelluksista.

Kokeile sitä ilmaiseksi.

Vastaa

Sähköpostiosoitettasi ei julkaista.