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 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.
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:
errors.New()
en fmt.Errorf()
vir die generering van eenvoudige foute.fmt.Errorf()
en die %w
werkwoord.errors.Join()
voeg verskeie foute saam in 'n enkele een.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:
errors.New()
of fmt.Errorf()
.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.
Oral is foute verklaar
gorm.ErrRecordNotFound
of user.ErrNotFound
of albei?
Ewekansige foutomvou het gelei tot inkonsekwente en arbitrêre logs
unexpected gorm error: failed to find business channel: error received when invoking API: unexpected: context canceled
Geen standaardisering het tot onbehoorlike fouthantering gelei nie
Geen kategorisering het monitering onmoontlik gemaak nie
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.Om die groeiende uitdagings aan te spreek, het ons besluit om 'n beter foutstrategie te bou rondom die kerngedagte van gesentraliseerde en gestruktureerde foutkodes .
Error
met 'n omvattende stel helpers.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."DEPS.PG.NOT_FOUND
deel, wat beteken "Rekord nie in PostgreSQL gevind nie."
Elke laag diens of biblioteek moet slegs sy eie naamruimtekodes terugstuur .
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 .
error
) en ons interne Error
.
'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.
trace_id
, en het geen idee waar dit vandaan kom nie.
Wanneer foute oor diensgrens gestuur word, word slegs die boonste vlak foutkode blootgestel.
Vir eksterne foute, hou aan om die huidige Protobuf ErrorCode en ErrorType te gebruik.
Outomatiseer naamruimtefoutkodes na Protobuf-kodes, HTTP-statuskodes en etikette.
ErrorCode
, ErrorType
, gRPC-status, HTTP-status en etikette vir aanteken/metrieke.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 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.
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
.Code
ontbloot metodes soos New()
of Wrap()
om 'n nuwe fout te skep.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 'n fout met behulp van With()
.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}
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.
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.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))
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.
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.
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! 😊
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 👋