paint-brush
Voorkom dat foute met hierdie nuwe raamwerk groeideur@olvrng
270 lesings

Voorkom dat foute met hierdie nuwe raamwerk groei

deur Oliver Nguyen30m2024/12/11
Read on Terminal Reader

Te lank; Om te lees

Dit is die verhaal van hoe ons met 'n eenvoudige fouthanteringsbenadering begin het, deeglik gefrustreerd geraak het namate die probleme toegeneem het, en uiteindelik ons eie foutraamwerk gebou het.
featured image - Voorkom dat foute met hierdie nuwe raamwerk groei
Oliver Nguyen HackerNoon profile picture
0-item
1-item

Die hantering van foute in Go is eenvoudig en buigsaam – dog geen struktuur nie!


Dit is veronderstel om eenvoudig te wees, reg? Stuur net 'n error terug , toegedraai met 'n boodskap, en gaan aan. Wel, daardie eenvoud verander vinnig in chaoties namate ons kodebasis groei met meer pakkette, meer ontwikkelaars en meer "vinnige oplossings" wat vir ewig daar bly. Met verloop van tyd is die logboeke vol "failed to do this" en "unexpected that", en niemand weet of dit die gebruiker se skuld, die bediener se fout, buggy-kode is, of dit is net 'n wanbelyning van die sterre nie!


Foute word geskep met inkonsekwente boodskappe. Elke pakket het sy eie stel style, konstantes of pasgemaakte fouttipes. Foutkodes word arbitrêr bygevoeg. Geen maklike manier om te sê watter foute van watter funksie af teruggestuur kan word sonder om in die implementering daarvan te delf nie!


So, ek het die uitdaging aanvaar om 'n nuwe foutraamwerk te skep. Ons het besluit om met 'n gestruktureerde, gesentraliseerde stelsel te gaan wat naamruimtekodes gebruik om foute sinvol, naspeurbaar te maak en – bowenal – ons gemoedsrus te gee!


Dit is die verhaal van hoe ons met 'n eenvoudige fouthanteringsbenadering begin het, deeglik gefrustreerd geraak het namate die probleme toegeneem het, en uiteindelik ons eie foutraamwerk gebou het. Die ontwerpbesluite, hoe dit geïmplementeer word, die lesse wat geleer is, en hoekom dit ons benadering tot die bestuur van foute verander het. Ek hoop dat dit 'n paar idees vir jou ook sal bring!


Go-foute is net waardes

Go het 'n eenvoudige manier om foute te hanteer: foute is net waardes. 'n Fout is net 'n waarde wat die error met 'n enkele metode Error() string implementeer. In plaas daarvan om 'n uitsondering te gooi en die huidige uitvoeringvloei te ontwrig, gee Go-funksies 'n error saam met ander resultate. Die oproeper kan dan besluit hoe om dit te hanteer: kontroleer die waarde daarvan om besluit te neem, omvou met nuwe boodskappe en konteks, of bloot die fout terugstuur, wat die hanteringslogika vir ouerbellers laat.


Ons kan enige tipe 'n error maak deur die Error() string daarop by te voeg. Hierdie buigsaamheid laat elke pakket toe om sy eie fouthanteringstrategie te definieer, en kies wat ook al die beste vir hulle werk. Dit integreer ook goed met Go se filosofie van saamstelbaarheid, wat dit maklik maak om foute te verpak, uit te brei of aan te pas soos vereis.

Elke pakket moet foute hanteer

Die algemene praktyk is om 'n foutwaarde terug te gee wat die error implementeer en die beller laat besluit wat om volgende te doen. Hier is 'n tipiese voorbeeld:

 func loadCredentials() (Credentials, error) { data, err := os.ReadFile("cred.json") if errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("file not found: %w", err) } if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } cred, err := verifyCredentials(cred); if err != nil { return nil, fmt.Errorf("invalid credentials: %w", err) } return cred, nil }

Go bied 'n handvol nutsprogramme om met foute te werk:

  • Skep van foute: errors.New() en fmt.Errorf() vir die generering van eenvoudige foute.
  • Omvoufoute: Omvou foute met addisionele konteks deur gebruik te maak van fmt.Errorf() en die %w werkwoord.
  • Kombineer foute: errors.Join() voeg verskeie foute saam in 'n enkele een.
  • Kontroleer en hanteer foute: errors.Is() pas 'n fout met 'n spesifieke waarde, errors.As() pas 'n fout by 'n spesifieke tipe, en errors.Unwrap() haal die onderliggende fout.


In die praktyk sien ons gewoonlik hierdie patrone:

  • Gebruik standaardpakkette: Stuur eenvoudige foute terug met errors.New() of fmt.Errorf() .
  • Uitvoer van konstantes of veranderlikes: Go-redis en gorm.io definieer byvoorbeeld herbruikbare foutveranderlikes.
  • Pasgemaakte fouttipes: Biblioteke soos lib/pq grpc/status.Error skep gespesialiseerde fouttipes, dikwels met gepaardgaande kodes vir addisionele konteks.
  • Foutkoppelvlakke met implementerings: Die aws-sdk-go gebruik 'n koppelvlakgebaseerde benadering om fouttipes met verskeie implementerings te definieer.
  • Of veelvuldige koppelvlakke: Soos Docker se errdefs , wat veelvuldige koppelvlakke definieer om foute te klassifiseer en te bestuur.

Ons het met 'n gemeenskaplike benadering begin

In die vroeë dae, soos baie Go-ontwikkelaars, het ons Go se algemene praktyke gevolg en fouthantering minimaal maar funksioneel gehou. Dit het goed genoeg gewerk vir 'n paar jaar.

  • Sluit stacktrace in met behulp van pkg/errors , 'n gewilde pakket op daardie tydstip.

  • Voer konstantes of veranderlikes uit vir pakketspesifieke foute.

  • Gebruik errors.Is() om te kyk vir spesifieke foute.

  • Omvou foute met 'n nuwe boodskap en konteks.

  • Vir API-foute definieer ons fouttipes en -kodes met Protobuf enum.


Insluitend stacktrace met pkg/errors

Ons het pkg/errors , 'n gewilde fouthanteringspakket destyds, gebruik om stacktrace by ons foute in te sluit. Dit was veral nuttig vir ontfouting, aangesien dit ons in staat gestel het om die oorsprong van foute oor verskillende dele van die toepassing op te spoor.

Om foute met stacktrace te skep, omvou en te versprei, het ons funksies soos Newf() , NewValuef() en Wrapf() geïmplementeer. Hier is 'n voorbeeld van ons vroeë implementering:

 type xError struct { msg message, stack: callers(), } func Newf(msg string, args ...any) error { return &xError{ msg: fmt.Sprintf(msg, args...), stack: callers(), // 👈 stacktrace } } func NewValuef(msg string, args ...any) error { return fmt.Errorf(msg, args...) // 👈 no stacktrace } func Wrapf(err error, msg string, args ...any) error { if err == nil { return nil } stack := getStack(err) if stack == nil { stack = callers() } return &xError{ msg: fmt.Sprintf(msg, args...), stack: stack, } }


Voer tans foutveranderlikes uit

Elke pakket in ons kodebasis het sy eie foutveranderlikes gedefinieer, dikwels met inkonsekwente style.

 package database var ErrNotFound = errors.NewValue("record not found") var ErrMultipleFound = errors.NewValue("multiple records found") var ErrTimeout = errors.NewValue("request timeout")
 package profile var ErrUserNotFound = errors.NewValue("user not found") var ErrBusinessNotFound = errors.NewValue("business not found") var ErrContextCancel = errors.NewValue("context canceled")


Kontroleer foute met errors.Is() en omvou met addisionele konteks

 res, err := repo.QueryUser(ctx, req) switch { case err == nil: // continue case errors.Is(database.NotFound): return nil, errors.Wrapf(ErrUserNotFound, "user not found (id=%v)", req.UserID) default: return nil, errors.Wrapf(ctx, "failed to query user (id=%v)", req.UserID) }

Dit het gehelp om foute met meer detail te versprei, maar het dikwels gelei tot breedsprakigheid, duplisering en minder duidelikheid in logs:

 internal server error: failed to query user: user not found (id=52a0a433-3922-48bd-a7ac-35dd8972dfe5): record not found: not found


Definieer eksterne foute met Protobuf

Vir eksterne-gerigte API's het ons 'n Protobuf-gebaseerde foutmodel aangeneem, geïnspireer deurMeta se Graph API :

 message Error { string message = 1; ErrorType type = 2; ErrorCode code = 3; string user_title = 4; string user_message = 5; string trace_id = 6; map<string, string> details = 7; } enum ErrorType { ERROR_TYPE_UNSPECIFIED = 1; ERROR_TYPE_AUTHENTICATION = 2; ERROR_TYPE_INVALID_REQUEST = 3; ERROR_TYPE_RATE_LIMIT = 4; ERROR_TYPE_BUSINESS_LIMIT = 5; ERROR_TYPE_WEBHOOK_DELIVERY = 6; } enum ErrorCode { ERROR_CODE_UNSPECIFIED = 1 [(error_type = UNSPECIFIED)]; ERROR_CODE_UNAUTHENTICATED = 2 [(error_type = AUTHENTICATION)]; ERROR_CODE_CAMPAIGN_NOT_FOUND = 3 [(error_type = NOT_FOUND)]; ERROR_CODE_META_CHOSE_NOT_TO_DELIVER = 4 /* ... */; ERROR_CODE_MESSAGE_WABA_TEMPLATE_CAN_ONLY_EDIT_ONCE_IN_24_HOURS = 5; }

Hierdie benadering het gehelp om foute te struktureer, maar met verloop van tyd is fouttipes en -kodes bygevoeg sonder 'n duidelike plan, wat tot inkonsekwenthede en duplisering gelei het.


En probleme het mettertyd gegroei

Oral is foute verklaar

  • Elke pakket het sy eie foutkonstantes gedefinieer met geen gesentraliseerde stelsel nie.
  • Konstante en boodskappe is oor die kodebasis versprei, wat dit onduidelik maak watter foute 'n funksie kan terugstuur – ugh, is dit gorm.ErrRecordNotFound of user.ErrNotFound of albei?


Ewekansige foutomvou het gelei tot inkonsekwente en arbitrêre logs

  • Baie funksies het foute toegedraai met arbitrêre, inkonsekwente boodskappe sonder om hul eie fouttipes te verklaar.
  • Logs was breedvoerig, oorbodig en moeilik om te soek of te monitor.
  • Foutboodskappe was generies en het dikwels nie verduidelik wat verkeerd geloop het of hoe dit gebeur het nie. Ook bros en geneig tot onopgemerkte veranderinge.
 unexpected gorm error: failed to find business channel: error received when invoking API: unexpected: context canceled


Geen standaardisering het tot onbehoorlike fouthantering gelei nie

  • Elke pakket het foute anders hanteer, wat dit moeilik gemaak het om te weet of 'n funksie foute teruggestuur, toegedraai of getransformeer het.
  • Konteks het dikwels verlore gegaan namate foute gepropageer het.
  • Boonste lae het vae 500 interne bedienerfoute ontvang sonder duidelike hoofoorsake.


Geen kategorisering het monitering onmoontlik gemaak nie

  • Foute is nie volgens erns of gedrag geklassifiseer nie: 'n context.Canceled fout kan 'n normale gedrag wees wanneer die gebruiker die blaaieroortjie toemaak, maar dit is belangrik as die versoek gekanselleer word omdat daardie navraag lukraak stadig is.
  • Belangrike kwessies is onder raserige stompe begrawe, wat dit moeilik gemaak het om te identifiseer.
  • Sonder kategorisering was dit onmoontlik om foutfrekwensie, erns of impak effektief te monitor.

Dit is tyd om fouthantering te sentraliseer

Terug na die tekenbord

Om die groeiende uitdagings aan te spreek, het ons besluit om 'n beter foutstrategie te bou rondom die kerngedagte van gesentraliseerde en gestruktureerde foutkodes .

  • Foute word oral verklaar → Sentraliseer foutverklarings op 'n enkele plek vir beter organisasie en naspeurbaarheid.
  • Inkonsekwente en arbitrêre logboeke → Gestruktureerde foutkodes met duidelike en konsekwente formatering.
  • Onbehoorlike fouthantering → Standaardiseer foutskepping en kontrolering van die nuwe Error met 'n omvattende stel helpers.
  • Geen kategorisering → Kategoriseer foutkodes met etikette vir effektiewe monitering deur logs en statistieke.

Ontwerp besluite

Alle foutkodes word op 'n gesentraliseerde plek met naamruimtestruktuur gedefinieer.

Gebruik naamspasies om duidelike, betekenisvolle en uitbreidbare foutkodes te skep. Voorbeeld:

  • PRFL.USR.NOT_FOUND vir "Gebruiker nie gevind nie."
  • FLD.NOT_FOUND vir "Vloeidokument nie gevind nie."
  • Albei kan 'n onderliggende basiskode DEPS.PG.NOT_FOUND deel, wat beteken "Rekord nie in PostgreSQL gevind nie."


Elke laag diens of biblioteek moet slegs sy eie naamruimtekodes terugstuur .

  • Elke laag diens, bewaarplek of biblioteek verklaar sy eie stel foutkodes.
  • Wanneer 'n laag 'n fout van 'n afhanklikheid ontvang, moet dit dit met sy eie naamspasiekode omvou voordat dit teruggestuur word.
  • Byvoorbeeld: Wanneer 'n fout gorm.ErrRecordNotFound van 'n afhanklikheid ontvang word, moet die "databasis"-pakket dit toevou as DEPS.PG.NOT_FOUND . Later moet die "profiel/gebruiker"-diens dit weer omvou as PRFL.USR.NOT_FOUND .


Alle foute moet die Error koppelvlak implementeer .

  • Dit skep 'n duidelike grens tussen foute van derdeparty-biblioteke ( error ) en ons interne Error .
  • Dit help ook vir migrasievordering, om te skei tussen gemigreerde pakkette en wat nog nie gemigreer is nie.


'n Fout kan een of meer foute omvou. Saam vorm hulle 'n boom.

 [FLD.INVALID_ARGUMENT] invalid argument → [TPL.INVALID_PARAMS] invalid input params 1. [TPL.PARAM.EMPTY] name can not be empty 2. [TPL.PARAM.MALFORM] invalid format for param[2]


Vereis altyd context.Context . Kan konteks aan die fout heg.

  • Ons het baie keer logs gesien met selfstandige foute met geen konteks, geen trace_id , en het geen idee waar dit vandaan kom nie.
  • Kan addisionele sleutel/waarde aan foute heg, wat in logs of monitering gebruik kan word.


Wanneer foute oor diensgrens gestuur word, word slegs die boonste vlak foutkode blootgestel.

  • Die bellers hoef nie die interne implementeringsbesonderhede van daardie diens te sien nie.


Vir eksterne foute, hou aan om die huidige Protobuf ErrorCode en ErrorType te gebruik.

  • Dit verseker terugwaartse versoenbaarheid, so ons kliënte hoef nie hul kode te herskryf nie.


Outomatiseer naamruimtefoutkodes na Protobuf-kodes, HTTP-statuskodes en etikette.

  • Ingenieurs definieer die kartering op die gesentraliseerde plek, en die raamwerk sal elke foutkode karteer na die ooreenstemmende Protobuf ErrorCode , ErrorType , gRPC-status, HTTP-status en etikette vir aanteken/metrieke.
  • Dit verseker konsekwentheid en verminder duplisering.

Die naamruimtefoutraamwerk

Kernpakkette en tipes

Daar is 'n paar kernpakkette wat die grondslag vorm van ons nuwe fouthanteringsraamwerk.

connectly.ai/go/pkgs/

  • errors : Die hoofpakket wat die Error en -kodes definieer.
  • errors/api : Vir die stuur van foute na die front-end of eksterne API.
  • errors/E : Helperpakket wat bedoel is om gebruik te word met puntinvoer.
  • testing : Toets nutsprogramme om met naamruimtefoute te werk.


Error en Code

Die Error is 'n uitbreiding van die standaard error , met bykomende metodes om 'n Code terug te gee. 'n Code word as 'n uint16 geïmplementeer.

 package errors // import "connectly.ai/go/pkgs/errors" type Error interface { error Code() Code } type Code struct { code uint16 } type CodeI interface { CodeDesc() CodeDesc } type GroupI interface { /* ... */ } type CodeDesc struct { /* ... */ }


Pakketfoute errors/E voer alle foutkodes en algemene tipes uit

 package E // import "connectly.ai/go/pkgs/errors/E" import "connectly.ai/go/pkgs/errors" type Error = errors.Error var ( DEPS = errors.DEPS PRFL = errors.PRFL ) func MapError(ctx context.Context, err error) errors.Mapper { /* ... */ } func IsErrorCode(err error, codes ...errors.CodeI) { /* ... */ } func IsErrorGroup(err error, groups ...errors.GroupI) { /* ... */ }

Voorbeeld gebruik

Voorbeeld foutkodes:

 // dependencies → postgres DEPS.PG.NOT_FOUND DEPS.PG.UNEXPECTED // sdk → hash SDK.HASH.UNEXPECTED // profile → user PRFL.USR.NOT_FOUND PFRL.USR.UNKNOWN // profile → user → repository PRFL.USR.REPO.NOT_FOUND PRFL.USR.REPO.UNKNOWN // profile → auth PRFL.AUTH.UNAUTHENTICATED PRFL.AUTH.UNKNOWN PRFL.AUTH.UNEXPECTED


Pakket database :

 package database // import "connectly.ai/go/pkgs/database" import "gorm.io/gorm" import . "connectly.ai/go/pkgs/errors/E" type DB struct { gorm: gorm.DB } func (d *DB) Exec(ctx context.Context, sql string, params ...any) *DB { tx := d.gorm.WithContext(ctx).Exec(sql, params...) return wrapTx(tx) } func (x *DB) Error(msgArgs ...any) Error { return wrapError(tx.Error()) // 👈 convert gorm error to 'Error' } func (x *DB) SingleRowError(msgArgs ...any) Error { if err := x.Error(); err != nil { return err } switch { case x.RowsAffected == 1: return nil case x.RowsAffected == 0: return DEPS.PG.NOT_FOUND.CallerSkip(1). New(x.Context(), formatMsgArgs(msgArgs)) default: return DEPS.PG.UNEXPECTED.CallerSkip(1). New(x.Context(), formatMsgArgs(msgArgs)) } }


Pakket pb/services/profile :

 package profile // import "connectly.ai/pb/services/profile" // these types are generated from services/profile.proto type QueryUserRequest struct { BusinessId string UserId string } type LoginRequest struct { Username string Password string }


service/profile :

 package profile import uuid "github.com/google/uuid" import . "connectly.ai/go/pkgs/errors/E" import l "connectly.ai/go/pkgs/logging/l" import profilepb "connectly.ai/pb/services/profile" // repository requests type QueryUserByUsernameRequest struct { Username string } // repository layer → query user func (r *UserRepository) QueryUserByUsernameAuth( ctx context.Context, req *QueryUserByUsernameRequest, ) (*User, Error) { if req.Username == "" { return PRFL.USR.REPO.INVALID_ARGUMENT.New(ctx, "empty request") } var user User sqlQuery := `SELECT * FROM "user" WHERE username = ? LIMIT 1` tx := r.db.Exec(ctx, sqlQuery, req.Username).Scan(&user) err := tx.SingleRowError() switch { case err == nil: return &user, nil case IsErrorCode(DEPS.PG.NOT_FOUND): return PRFL.USR.REPO.USER_NOT_FOUND. With(l.String("username", req.Username)) Wrap(ctx, "user not found") default: return PRFL.USR.REPO.UNKNOWN. Wrap(ctx, "failed to query user") } } // user service layer → query user func (u *UserService) QueryUser( ctx context.Context, req *profilepb.QueryUserRequest, ) (*profilepb.QueryUserResponse, Error) { // ... rr := QueryUserByUsernameRequest{ Username: req.Username } err := u.repo.QueryUserByUsername(ctx, rr) if err != nil { return nil, MapError(ctx, err). Map(PRFL.USR.REPO.NOT_FOUND, PRFL.USR.NOT_FOUND, "the user %q cannot be found", req.UserName, api.UserTitle("User Not Found"), api.UserMsg("The requested user id %q can not be found", req.UserId)). KeepGroup(PRFL.USR). Default(PRFL.USR.UNKNOWN, "failed to query user") } // ... return resp, nil } // auth service layer → login user func (a *AuthService) Login( ctx context.Context, req *profilepb.LoginRequest, ) (*profilepb.LoginResponse, *profilepb.LoginResponse, Error) { vl := PRFL.AUTH.INVALID_ARGUMENT.WithMsg("invalid request") vl.Vl(req.Username != "", "no username", api.Detail("username is required")) vl.Vl(req.Password != "", "no password", api.Detail("password is required")) if err := vl.ToError(ctx); err != nil { return err } hashpwd, err := hash.Hash(req.Password) if err != nil { return PRFL.AUTH.UNEXPECTED.Wrap(ctx, err, "failed to calc hash") } usrReq := profilepb.QueryUserByUsernameRequest{/*...*/} usrRes, err := a.userServiceClient.QueryUserByUsername(ctx, usrReq) if err != nil { return nil, MapError(ctx, err). Map(PRFL.USR.NOT_FOUND, PRFL.AUTH.UNAUTHENTICATED, "unauthenticated"). Default(PRFL.AUTH.UNKNOWN, "failed to query by username") } // ... }

Wel, daar is baie nuwe funksies en konsepte in die bogenoemde kode. Kom ons gaan stap vir stap deur hulle.

Skep en omvou foute

Voer eers errors/E in met behulp van puntinvoer

Dit sal jou toelaat om algemene tipes soos Error in plaas van errors.Error direk te gebruik en toegang tot kodes deur PRFL.USR.NOT_FOUND in plaas van errors.PRFL.USR.NOT_FOUND .

 import . "connectly.ai/go/pkgs/errors/E"


Skep nuwe foute met CODE.New()

Gestel jy kry 'n ongeldige versoek, jy kan 'n nuwe fout skep deur:

 err := PRFL.USR.INVALID_ARGUMENT.New(ctx, "invalid request")
  • PRFL.USR.INVALID_ARGUMENT is 'n Code .
  • 'n Code ontbloot metodes soos New() of Wrap() om 'n nuwe fout te skep.
  • Die New() funksie ontvang context.Context as die eerste argument, gevolg deur boodskap en opsionele argumente.


Druk dit met fmt.Print(err) :

 [PRFL.USR.INVALID_ARGUMENT] invalid request


of met fmt.Printf("%+v") om meer besonderhede te sien:

 [PRFL.USR.INVALID_ARGUMENT] invalid request connectly.ai/go/services/profile.(*UserService).QueryUser /usr/i/src/go/services/profile/user.go:1234 connectly.ai/go/services/profile.(*UserRepository).QueryUser /usr/i/src/go/services/profile/repo/user.go:2341


Wikkel 'n fout in 'n nuwe fout met CODE.Wrap()

 dbErr := DEPS.PG.NOT_FOUND.Wrap(ctx, gorm.ErrRecordNotFound, "not found") usrErr := PRFL.USR.NOT_FOUND.Wrap(ctx, dbErr, "user not found")


sal hierdie uitvoer produseer met fmt.Print(usrErr) :

 [PRFL.USR.NOT_FOUND] user not found → [DEPS.PG.NOT_FOUND] not found → record not found


of met fmt.Printf("%+v", usrErr)

 [PRFL.USR.NOT_FOUND] user not found → [DEPS.PG.NOT_FOUND] not found → record not found connectly.ai/go/services/profile.(*UserService).QueryUser /usr/i/src/go/services/profile/user.go:1234


Die stapelspoor sal van die binneste Error kom. As jy 'n helperfunksie skryf, kan jy CallerSkip(skip) gebruik om rame oor te slaan:

 func mapUserError(ctx context.Context, err error) Error { switch { case IsErrorCode(err, DEPS.PG.NOT_FOUND): return PRFL.USR.NOT_FOUND.CallerSkip(1).Wrap(ctx, err, "...") default: return PRFL.USR.UNKNOWN.CallerSkip(1).Wrap(ctx, err, "...") } }

Voeg konteks by foute

Voeg konteks by 'n fout met behulp van With()

  • Jy kan addisionele sleutel/waarde-pare by foute voeg deur .With(l.String(...)) .
  • logging/l is 'n helperpakket om suikerfunksies vir logging uit te voer.
  • l.String("flag", flag) gee 'n Tag{String: flag} en l.UUID("user_id, userID) gee Tag{Stringer: userID} terug.
 import l "connectly.ai/go/pkgs/logging/l" usrErr := PRFL.USR.NOT_FOUND. With(l.UUID("user_id", req.UserID), l.String("flag", flag)). Wrap(ctx, dbErr, "user not found")


Die etikette kan uitgevoer word met fmt.Printf("%+v", usrErr) :

 [PRFL.USR.NOT_FOUND] user not found {"user_id": "81febc07-5c06-4e01-8f9d-995bdc2e0a9a", "flag": "ABRW"} → [DEPS.PG.NOT_FOUND] not found {"a number": 42} → record not found


Voeg konteks by foute direk binne New() , Wrap() , of MapError() :

Deur gebruik te maak van l.String() funksie en sy familie, kan New() en soortgelyke funksies merkers slim opspoor tussen formateringsargumente. Dit is nie nodig om verskillende funksies bekend te stel nie.

 err := INF.HEALTH.NOT_READY.New(ctx, "service %q is not ready (retried %v times)", req.ServiceName, l.String("flag", flag) countRetries, l.Number("count", countRetries), )


sal uitvoer:

 [INF.HEALTH.NOT_READY] service "magic" is not ready (retried 2 times) {"flag": "ABRW", "count": 2}

Verskillende tipes: Error0 , VlError , ApiError

Tans is daar 3 tipes wat die Error koppelvlakke implementeer. Jy kan meer tipes byvoeg indien nodig. Elkeen kan verskillende struktuur hê, met pasgemaakte metodes vir spesifieke behoeftes.


Error is 'n uitbreiding van Go se standaard error

 type Error interface { error Code() Message() Fields() []tags.Field StackTrace() stacktrace.StackTrace _base() *base // a private method }


Dit bevat 'n private metode om te verseker dat ons nie per ongeluk nuwe Error buite die errors implementeer nie. Ons mag (of mag nie) daardie beperking in die toekoms ophef wanneer ons meer gebruikspatrone ervaar.


Hoekom gebruik ons nie net die standaard error en gebruik tipe bewering nie?

Omdat ons wil skei tussen derdepartyfoute en ons interne foute. Alle lae en pakkette in ons interne kodes moet altyd Error terugstuur. Op hierdie manier kan ons veilig weet wanneer ons derdepartyfoute moet omskakel, en wanneer ons net ons interne foutkodes hoef te hanteer.


Dit skep ook 'n grens tussen gemigreerde pakkette en pakkette wat nog nie gemigreer is nie. Terug na die werklikheid, ons kan nie net 'n nuwe tipe verklaar, 'n towerstaffie swaai, 'n spelopdrag fluister, en dan word alle miljoene reëls kode magies omgeskakel en naatloos sonder foute werk nie! Nee, daardie toekoms is nog nie hier nie. Dit mag dalk eendag kom, maar vir nou moet ons steeds ons pakkette een vir een migreer.

Error0 is die verstek Error


Die meeste foutkodes sal 'n Error0 waarde produseer. Dit bevat 'n base en 'n opsionele sub-fout. Jy kan NewX() gebruik om 'n konkrete *Error0 struktuur in plaas van 'n Error -koppelvlak terug te gee, maar jy moet versigtig wees .

 type Error0 struct { base err error } var errA: Error = DEPS.PG.NOT_FOUND.New (ctx, "not found") var errB: *Error0 = DEPS.PG.NOT_FOUND.NewX(ctx, "not found")


base is die gemeenskaplike struktuur wat deur alle Error gedeel word, om algemene funksionaliteit te verskaf: Code() , Message() , StackTrace() , Fields() , en meer.


 type base struct { code Code msg string kv []tags.Field stack stacktrace.StackTrace }


VlError is vir valideringsfoute

Dit kan veelvuldige sub-foute bevat, en bied goeie metodes om met valideringshelpers te werk.

 type VlError struct { base errs []error }


Jy kan 'n VlError soortgelyk aan ander Error skep:

 err := PRFL.USR.INVALID_ARGUMENT.New(ctx, "invalid request")


Of maak 'n VlBuilder , voeg foute by en skakel dit dan om na 'n VlError :

 userID, err0 := parseUUID(req.UserId) err1 := validatePassword(req.Password) vl := PRFL.USR.INVALID_ARGUMENT.WithMsg("invalid request") vl.Add(err0, err1) vlErr := vl.ToError(ctx)


En sluit sleutel/waarde-pare soos gewoonlik in:

 vl := PRFL.USR.INVALID_ARGUMENT. With(l.Bool("testingenv", true)). WithMsg("invalid request") userID, err0 := parseUUID(req.UserId) err1 := validatePassword(req.Password) vl.Add(err0, err1) vlErr := vl.ToError(ctx, l.String("user_id", req.UserId))


Die gebruik van fmt.Printf("%+v", vlErr) sal uitvoer:

 [PRFL.USR.INVALID_ARGUMENT] invalid request {"testingenv": true, "user_id": "A1234567890"}


ApiError is 'n adapter vir die migrasie van API-foute

Voorheen het ons 'n aparte api.Error struktuur gebruik om API-foute aan die voorkant en eksterne kliënte terug te stuur. Dit bevat ErrorType as ErrorCode soos voorheen genoem .

 package api import errorpb "connectly.ai/pb/models/error" // Deprecated type Error struct { pbType errorpb.ErrorType pbCode errorpb.ErrorCode cause error msg string usrMsg string usrTitle string // ... }


Hierdie tipe is nou opgeskort. In plaas daarvan sal ons al die kartering ( ErrorType , ErrorCode , gRPC-kode, HTTP-kode) in 'n sentraliseerplek verklaar en dit by ooreenstemmende grense omskakel. Ek sal in die volgende afdeling oor kodeverklaring bespreek.


Om die migrasie na die nuwe naamruimtefoutraamwerk te doen, het ons 'n tydelike naamruimte ZZZ.API_TODO bygevoeg. Elke ErrorCode word 'n ZZZ.API_TODO -kode.

 ZZZ.API_TODO.UNEXPECTED ZZZ.API_TODO.INVALID_REQUEST ZZZ.API_TODO.USERNAME_ ZZZ.API_TODO.META_CHOSE_NOT_TO_DELIVER ZZZ.API_TODO.MESSAGE_WABA_TEMPLATE_CAN_ONLY_EDIT_ONCE_IN_24_HOURS


En ApiError is geskep as 'n adapter. Alle funksies wat voorheen *api.Error terugstuur, is verander om eerder Error terug te gee (geïmplementeer deur *ApiError ).

 package api import . "connectly.ai/go/pkgs/errors/E" // previous func FailPreconditionf(err error, msg string, args ...any) *Error { return &Error{ pbType: ERROR_TYPE_FAILED_PRECONDITION, pbCode: ERROR_CODE_MESSAGE_WABA_TEMPLATE_CAN_ONLY_EDIT_ONCE_IN_24_HOURS, cause: err, msg: fmt.Sprintf(msg, args...) } } // current: this is deprecated, and serves and an adapter func FailPreconditionf(err error, msg string, args ...any) *Error { ctx := context.TODO() return ZZZ.API_TODO.MESSAGE_WABA_TEMPLATE_CAN_ONLY_EDIT_ONCE_IN_24_HOURS. CallerSkip(1). // correct the stacktrace by 1 frame Wrap(ctx, err, msg, args...) }


Wanneer al die migrasie voltooi is, is die vorige gebruik:

 wabaErr := verifyWabaTemplateStatus(tpl) apiErr := api.FailPreconditionf(wabaErr, "template cannot be edited"). WithErrorCode(ERROR_CODE_MESSAGE_WABA_TEMPLATE_CAN_ONLY_EDIT_ONCE_IN_24_HOURS). WithUserMsg("According to WhatsApp, the message template can be only edited once in 24 hours. Consider creating a new message template instead."). ErrorOrNil()


moet word:

 CPG.TPL.EDIT_ONCE_IN_24_HOURS.Wrap( wabaErr, "template cannot be edited", api.UserMsg("According to WhatsApp, the message template can be only edited once in 24 hours. Consider creating a new message template instead."))


Let daarop dat die ErrorCode implisiet afgelei is van die interne naamruimtekode. Dit is nie nodig om dit elke keer uitdruklik toe te ken nie. Maar hoe om die verhouding tussen kodes te verklaar? Dit sal in die volgende afdeling verduidelik word.

Verklaar nuwe foutkodes

Op hierdie stadium weet jy reeds hoe om nuwe foute uit bestaande kodes te skep. Dit is tyd om te verduidelik oor kodes en hoe om 'n nuwe een by te voeg.


'n Code word geïmplementeer as 'n uint16 -waarde, wat 'n ooreenstemmende string-aanbieding het.

 type Code struct { code: uint16 } fmt.Printf("%q", DEPS.PG.NOT_FOUND) // "DEPS.PG.NOT_FOUND"


Om daardie stringe te stoor, is daar 'n verskeidenheid van alle beskikbare CodeDesc :

 const MaxCode = 321 // 👈 this value is generated var allCodes [MaxCode]CodeDesc type CodeDesc { c int // 42 code string // DEPS.PG.NOT_FOUND api APICodeDesc } type APICodeDesc { ErrorType errorpb.ErrorType ErrorCode errorpb.ErrorCode HttpCode int DefMessage string UserMessage string UserTitle string }


Hier is hoe kodes verklaar word:

 var DEPS deps // dependencies var PRFL prfl // profile var FLD fld // flow document type deps struct { PG pg // postgres RD rd // redis } // tag:postgres type pg struct { NOT_FOUND Code0 // record not found CONFLICT Code0 // record already exist MALFORM_SQL Code0 } // tag:profile type PRFL struct { REPO prfl_repo USR usr AUTH auth } // tag:profile type prfl_repo struct { NOT_FOUND Code0 // internal error code INVALID_ARGUMENT VlCode // internal error code } // tag:usr type usr struct { NOT_FOUND Code0 `api-code:"USER_NOT_FOUND"` INVALID_ARGUMENT VlCode `api-code:"INVALID_ARGUMENT"` DISABlED_ACCOUNT Code0 `api-code:"DISABLED_ACCOUNT"` } // tag:auth type auth struct { UNAUTHENTICATED Code0 `api-code:"UNAUTHENTICATED"` PERMISSION_DENIED Code0 `api-code:"PERMISSION_DENIED"` }


Nadat u nuwe kodes verklaar het, moet u die generasieskrip uitvoer :

 run gen-errors


Die gegenereerde kode sal soos volg lyk:

 // Code generated by error-codes. DO NOT EDIT. func init() { // ... PRFL.AUTH.UNAUTHENTICATED = Code0{Code{code: 143}} PRFL.AUTH.PERMISSION_DENIED = Code0{Code{code: 144}} // ... allCodes[143] = CodeDesc{ c: 143, code: "PRFL.AUTH.UNAUTHENTICATED", tags: []string{"auth", "profile"}, api: APICodeDesc{ ErrorType: ERROR_TYPE_UNAUTHENTICATED, ErrorCode: ERROR_CODE_UNAUTHENTICATED, HTTPCode: 401, DefMessage: "Unauthenticated error", UserMessage: "You are not authenticated.", UserTitle: "Unauthenticated error", })) }


Elke tipe Error het 'n ooreenstemmende Code tipe

Al ooit gewonder hoe PRFL.USR.NOT_FOUND.New() *Error0 skep en PRFL.USR.INVALID_ARGUMENTS.New() 'n *VlError ? Dit is omdat hulle verskillende kodetipes gebruik.


En elke Code gee verskillende Error terug, elkeen kan sy eie ekstra metodes hê:

 type Code0 struct { Code } type VlCode struct { Code } func (c Code0) New(/*...*/) Error { return &Error0{/*...*/} } func (c VlCode) New(/*...*/) Error { return &VlError{/*...*/} } // extra methods on VlCode to create VlBuilder func (c VlCode) WithMsg(msg string, args ...any) *VlBuilder {/*...*/} type VlBuilder struct { code VlCode msg string args []any } func (b *VlBuilder) ToError(/*...*/) Error { return &VlError{Code: code, /*...*/ } }


Gebruik api-code om die kodes wat beskikbaar is vir eksterne API te merk

  • Die naamruimtefoutkode moet intern gebruik word.

  • Om 'n kode beskikbaar te maak vir terugkeer in eksterne HTTP API, moet jy dit met api-code merk. Die waarde is die ooreenstemmende errorpb.ErrorCode .

  • As 'n foutkode nie met api-code gemerk is nie, is dit interne kode en sal dit as 'n generiese Internal Server Error gewys word.

  • Let daarop dat PRFL.USR.NOT_FOUND eksterne kode is, terwyl PRFL.USR.REPO.NOT_FOUND interne kode is.


Verklaar kartering tussen ErrorCode , ErrorType , en gRPC/HTTP kodes in protobuf deur gebruik te maak van enum opsie:

 // error/type.proto ERROR_TYPE_PERMISSION_DENIED = 707 [(error_type_detail_option) = { type: "PermissionDeniedError", grpc_code: PERMISSION_DENIED, http_code: 403, // Forbidden message: "permission denied", user_title: "Permission denied", user_message: "The caller does not have permission to execute the specified operation.", }]; // error/code.proto ERROR_CODE_DISABlED_ACCOUNT = 70020 [(error_code_detail_option) = { error_type: ERROR_TYPE_DISABlED_ACCOUNT, grpc_code: PERMISSION_DENIED, http_code: 403, // Forbidden message: "account is disabled", user_title: "Account is disabled", user_message: "Your account is disabled. Please contact support for more information.", }];

UNEXPECTED en UNKNOWN kodes

Elke laag het gewoonlik 2 generiese kodes UNEXPECTED en UNKNOWN . Hulle dien effens verskillende doeleindes:

  • UNEXPECTED kode word gebruik vir foute wat nooit moet gebeur nie.
  • UNKNOWN kode word gebruik vir foute wat nie eksplisiet hanteer word nie.

Kartering foute na nuwe kode

Wanneer u 'n fout ontvang wat van 'n funksie af teruggestuur word, moet u dit hanteer: derdepartyfoute omskakel na interne naamruimtefoute en kaartfoutkodes van binnelae na buitenste lae.


Skakel derdepartyfoute om na interne naamspasiefoute

Hoe u foute hanteer, hang af van: wat die derdeparty-pakket terugstuur en wat u toepassing benodig. Byvoorbeeld, wanneer databasis- of eksterne API-foute hanteer word:

 switch { case errors.Is(err, sql.ErrNoRows): // map a database "no rows" error to an internal "not found" error return nil, PRFL.USR.NOT_FOUND.Wrap(ctx, err, "user not found") case errors.Is(err, context.DeadlineExceeded): // map a context deadline exceeded error to a timeout error return nil, PRFL.USR.TIMEOUT.Wrap(ctx, err, "query timeout") default: // wrap any other error as unknown return nil, PRFL.USR.UNKNOWN.Wrap(ctx, err, "unexpected error") }


Gebruik helpers vir interne naamruimtefoute

  • IsErrorCode(err, CODES...) : Kontroleer of die fout enige van die gespesifiseerde kodes bevat.
  • IsErrorGroup(err, GROUP) : Gee waar as die fout aan die invoergroep behoort.


Tipiese gebruikspatroon:

 user, err := queryUser(ctx, userReq) switch { case err == nil: // continue case IsErrorCode(PRL.USR.REPO.NOT_FOUND): // check for specific error code and convert to external code // and return as HTTP 400 Not Found return nil, PRFL.USR.NOT_FOUND.Wrap(ctx, err, "user not found") case IsGroup(PRL.USR): // errors belong to the PRFL.USR group are returned as is return nil, err default: return nil, PRL.USR.UNKNOWN.Wrap(ctx, err, "failed to query user") }


MapError() vir die skryf van karteringkode makliker:

Aangesien kartering foutkodes 'n algemene patroon is, is daar 'n MapError() helper om die skryf van kode vinniger te maak. Die bogenoemde kode kan herskryf word as:

 user, err := queryUser(ctx, userReq) if err != nil { return nil, MapError(ctx, err). Map(PRL.USR.REPO.NOT_FOUND, PRFL.USR.NOT_FOUND, "user not found"). KeepGroup(PRF.USR). Default(PRL.USR.UNKNOWN, "failed to query user") }


Jy kan argumente formateer en sleutel/waarde-pare soos gewoonlik byvoeg:

 return nil, MapError(ctx, err). Map(PRL.USR.REPO.NOT_FOUND, PRFL.USR.NOT_FOUND, "user %v not found", username, l.String("flag", flag)). KeepGroup(PRF.USR). Default(PRL.USR.UNKNOWN, "failed to query user", l.Any("retries", retryCount))

Toets met naamruimte Error s

Toetsing is krities vir enige ernstige kodebasis. Die raamwerk bied gespesialiseerde helpers soos ΩxError() om skryf en beweer fouttoestande in toetse makliker en meer ekspressief te maak.

 // 👉 return true if the error contains the message ΩxError(err).Contains("not found") // 👉 return true if the error does not contain the message ΩxError(err).NOT().Contains("not found")


Daar is baie meer metodes, en jy kan hulle ook ketting:

 ΩxError(err). MatchCode(DEPS.PG.NOT_FOUND). // match any code in top or wrapped errors TopErrorMatchCode(PRFL.TPL.NOT_FOUND) // only match code from the top error MatchAPICode(API_CODE.WABA_TEMPLATE_NOTE_FOUND). // match errorpb.ErrorCode MatchExact("exact message to match")


Waarom metodes gebruik in plaas van Ω(err).To(testing.MatchCode()) ?

Omdat metodes meer ontdekbaar is. Wanneer jy gekonfronteer word met dosyne funksies soos testing.MatchValues() , is dit moeilik om te weet watter een met Error s sal werk en watter nie. Met metodes kan jy eenvoudig 'n punt tik . , en jou IDE sal alle beskikbare metodes lys wat spesifiek ontwerp is om Error te beweer.


Migrasie

Die raamwerk is net die helfte van die storie. Skryf die kode? Dis die maklike deel. Die werklike uitdaging begin wanneer jy dit in 'n massiewe, lewende kodebasis moet bring waar dosyne ingenieurs daagliks veranderinge aanbring, kliënte verwag dat alles perfek sal werk, en die stelsel net nie kan ophou loop nie.


Migrasie kom met verantwoordelikheid. Dit gaan daaroor om hare klein stukkies kode versigtig te verdeel, klein veranderinge op 'n slag te maak, 'n klomp toetse in die proses te breek. Inspekteer en herstel hulle dan een vir een met die hand, voeg saam in die hooftak, ontplooi na produksie, kyk na die logs en waarskuwings. Herhaal dit oor en oor...


Hier is 'n paar wenke vir migrasie wat ons langs die pad geleer het:


Begin met soek en vervang: Begin deur ou patrone met die nuwe raamwerk te vervang. Los enige samestellingskwessies op wat uit hierdie proses voortspruit.

Vervang byvoorbeeld alle error in hierdie pakket met Error .

 type ProfileController interface { LoginUser(req *LoginRequest) (*LoginResponse, error) QueryUser(req *QueryUserRequest) (*QueryUserResponse, error) }

Die nuwe kode sal soos volg lyk:

 import . "connectly.ai/go/pkgs/errors" type ProfileController interface { LoginUser(req *LoginRequest) (*LoginResponse, Error) QueryUser(req *QueryUserRequest) (*QueryUserResponse, Error) }


Migreer een pakket op 'n slag: Begin met die laagste vlak pakkette en werk jou pad op. Op hierdie manier kan u verseker dat die laervlak-pakkette ten volle gemigreer word voordat u na die hoërvlak-pakkette oorgaan.


Voeg ontbrekende eenheidstoetse by: As dele van die kodebasis nie toetse het nie, voeg dit by. As jy nie selfversekerd is in jou veranderinge nie, voeg meer toetse by. Hulle is nuttig om seker te maak dat jou veranderinge nie bestaande funksionaliteit breek nie.


As jou pakket afhanklik is van die oproep van hoërvlak-pakkette: Oorweeg dit om die verwante funksies te verander na DEEPRECATED en voeg dan nuwe funksies by met die nuwe Error .


Aanvaar dat jy die databasispakket migreer, wat die Transaction() metode het:

 package database func (db *DB) Transaction(ctx context.Context, fn func(tx *gorm.DB) error) error { return db.gorm.Transaction(func(tx *gorm.DB) error { return fn(tx) }) }


En dit word gebruik in die gebruikersdienspakket:

 err = s.DB(ctx).Transaction(func(tx *database.DB) error { user, usrErr := s.repo.CreateUser(ctx, tx, user) if usrErr != nil { return usrErr } }


Aangesien jy eers die database migreer, laat die user en dosyne ander pakkette net so. Die s.repo.CreateUser() -oproep gee steeds die ou error terug terwyl die Transaction() metode die nuwe Error tipe moet terugstuur. Jy kan die Transaction() metode verander na DEPRECATED en 'n nuwe TransactionV2() metode byvoeg:

 package database // DEPRECATED: use TransactionV2 instead func (db *DB) Transaction_DEPRECATED(ctx context.Context, fn func(tx *gorm.DB) error) error { return db.gorm.Transaction(func(tx *gorm.DB) error { return fn(tx) }) } func (db *DB) TransactionV2(ctx context.Context, fn func(tx *gorm.DB) error) Error { err := db.gorm.Transaction(func(tx *gorm.DB) error { return fn(tx) }) return adaptToErrorV2(err) }


Voeg nuwe foutkodes by terwyl jy gaan : Wanneer jy 'n fout teëkom wat nie by die bestaandes pas nie, voeg 'n nuwe kode by. Dit sal jou help om 'n omvattende stel foutkodes met verloop van tyd te bou. Kodes van ander pakkette is altyd beskikbaar as verwysings.


Gevolgtrekking

Fouthantering in Go kan aanvanklik eenvoudig voel—stuur net 'n error terug en gaan aan. Maar soos ons kodebasis gegroei het, het daardie eenvoud verander in 'n deurmekaar gemors van vae logs, inkonsekwente hantering en eindelose ontfoutingsessies.


Deur terug te tree en te heroorweeg hoe ons foute hanteer, het ons 'n stelsel gebou wat vir ons werk, nie teen ons nie. Gesentraliseerde en gestruktureerde naamruimtekodes gee ons duidelikheid, terwyl gereedskap vir kartering, omvou en toetsfoute ons lewens makliker maak. In plaas daarvan om deur die see van houtstompe te swem, het ons nou betekenisvolle, naspeurbare foute wat ons vertel wat fout is en waar om te kyk.


Hierdie raamwerk gaan nie net daaroor om ons kode skoner te maak nie; dit gaan daaroor om tyd te bespaar, frustrasie te verminder en ons te help voorberei vir die onbekende. Dit is net die begin van 'n reis - ons ontdek steeds meer patrone - maar die resultaat is 'n stelsel wat op een of ander manier gemoedsrus by fouthantering kan bring. Hopelik kan dit 'n paar idees vir jou projekte ook laat opvlam! 😊



Skrywer

Ek is Oliver Nguyen. 'n Sagtewarevervaardiger wat meestal in Go en JavaScript werk. Ek geniet dit om elke dag 'n beter weergawe van myself te leer en te sien. Spin af en toe nuwe oopbronprojekte af. Deel kennis en gedagtes tydens my reis.

Die plasing word ook gepubliseer by blog.connectly.ai en olivernguyen.io 👋