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 virheenw
tarjoamallaUnwrap
-metodin, joka palauttaaw
. Sekäe
ettäw
ovat ohjelmien käytettävissä, jolloine
voi antaa lisäkontekstinw
: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.