paint-brush
Anpeche erè yo grandi ak nouvo chapant sa apa@olvrng
270 lekti

Anpeche erè yo grandi ak nouvo chapant sa a

pa Oliver Nguyen30m2024/12/11
Read on Terminal Reader

Twò lontan; Pou li

Sa a se istwa a nan ki jan nou te kòmanse ak yon apwòch manyen erè senp, yo te byen fristre kòm pwoblèm yo te grandi, epi evantyèlman bati pwòp kad erè nou an.
featured image - Anpeche erè yo grandi ak nouvo chapant sa a
Oliver Nguyen HackerNoon profile picture
0-item
1-item

Manyen erè nan Go se senp ak fleksib - men pa gen okenn estrikti!


Li sipoze senp, pa vre? Jis retounen yon error , vlope ak yon mesaj, epi kontinye. Oke, senplisite sa a byen vit vire nan chaotic kòm baz kòd nou an ap grandi ak plis pakè, plis devlopè, ak plis "ranje rapid" ki rete la pou tout tan. Apre yon tan, mòso bwa yo plen ak "echwe pou fè sa" ak "inatanndi sa", epi pèsonn pa konnen si se fòt itilizatè a, fòt sèvè a, kòd buggy, oswa li se jis yon move aliyman nan zetwal yo!


Erè yo kreye ak mesaj enkonsistan. Chak pake gen pwòp seri estil, konstan, oswa kalite erè koutim. Kòd erè yo ajoute abitrèman. Pa gen yon fason fasil pou di ki erè yo ka retounen nan ki fonksyon san yo pa fouye nan aplikasyon li!


Se konsa, mwen te pran defi a nan kreye yon nouvo kad erè. Nou te deside ale ak yon sistèm estriktire, santralize ki sèvi ak kòd espas non pou fè erè yo gen sans, retrace, epi - sa ki pi enpòtan - ban nou lapè nan tèt ou!


Sa a se istwa a nan ki jan nou te kòmanse ak yon apwòch manyen erè senp, te byen fristre kòm pwoblèm yo te grandi, epi evantyèlman bati pwòp kad erè nou an. Desizyon konsepsyon yo, ki jan li aplike, leson yo aprann, ak poukisa li transfòme apwòch nou an nan jere erè. Mwen espere ke li pral pote kèk ide pou ou tou!


Go erè yo se jis valè

Go gen yon fason ki senp pou jere erè: erè yo se jis valè. Yon erè se jis yon valè ki aplike koòdone error a ak yon sèl metòd Error() string . Olye pou yo voye yon eksepsyon ak deranje koule nan ekzekisyon aktyèl la, fonksyon Go retounen yon valè error ansanm ak lòt rezilta. Lè sa a, moun k ap rele a ka deside ki jan yo okipe li: tcheke valè li yo pran desizyon, vlope ak nouvo mesaj ak kontèks, oswa tou senpleman retounen erè a, kite lojik nan manyen pou paran ki rele yo.


Nou ka fè nenpòt ki kalite error lè nou ajoute metòd Error() string sou li. Fleksibilite sa a pèmèt chak pake defini pwòp estrateji manyen erè li yo, epi chwazi sa ki pi bon pou yo. Sa a tou entegre byen ak filozofi Go a nan konpozisyon, sa ki fè li fasil vlope, pwolonje, oswa Customize erè jan sa nesesè.

Chak pake bezwen fè fas ak erè

Pratik komen an se retounen yon valè erè ki aplike koòdone error a epi kite moun kap rele a deside kisa pou l fè apre. Men yon egzanp tipik:

 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 bay yon ti ponyen sèvis piblik pou travay ak erè:

  • Kreye erè: errors.New() ak fmt.Errorf() pou jenere erè senp.
  • Anbalaj erè: Wrap erè ak kontèks adisyonèl lè l sèvi avèk fmt.Errorf() ak vèb %w la.
  • Konbine erè: errors.Join() fusion plizyè erè nan yon sèl.
  • Tcheke ak manyen erè: errors.Is() matche ak yon erè ak yon valè espesifik, errors.As() matche ak yon erè nan yon kalite espesifik, ak errors.Unwrap() rekipere erè ki kache.


Nan pratik, anjeneral nou wè modèl sa yo:

  • Sèvi ak pakè estanda: Retounen erè senp ak errors.New() oswa fmt.Errorf() .
  • Ekspòtasyon konstan oswa varyab: Pou egzanp, go-redis ak gorm.io defini varyab erè ki kapab itilize ankò.
  • Kalite erè Customized: Bibliyotèk tankou lib/pq grpc/status.Error kreye kalite erè espesyalize, souvan ak kòd ki asosye pou kontèks adisyonèl.
  • Koòdone erè ak enplemantasyon: aws-sdk-go itilize yon apwòch ki baze sou koòdone pou defini kalite erè ak aplikasyon divès kalite.
  • Oswa plizyè koòdone: Tankou errdefs Docker a , ki defini plizyè koòdone pou klase ak jere erè.

Nou te kòmanse ak yon apwòch komen

Nan premye jou yo, tankou anpil devlopè Go, nou te swiv pratik komen Go yo epi kenbe erè jere minimòm men fonksyonèl. Li te travay byen ase pou yon koup de ane.

  • Mete stacktrace lè l sèvi avèk pkg/errors , yon pake popilè nan moman sa a.

  • Ekspòte konstan oswa varyab pou erè pakè espesifik.

  • Sèvi ak errors.Is() pou tcheke erè espesifik.

  • Mete erè ak yon nouvo mesaj ak yon kontèks.

  • Pou erè API, nou defini kalite erè ak kòd ak Protobuf enum.


Ki gen ladan stacktrace ak pkg/errors

Nou te itilize pkg/errors , yon pake popilè pou jere erè nan moman sa a, pou mete stacktrace nan erè nou yo. Sa a te patikilyèman itil pou debogaj, paske li te pèmèt nou trase orijin erè atravè diferan pati nan aplikasyon an.

Pou kreye, vlope, ak difize erè ak stacktrace, nou aplike fonksyon tankou Newf() , NewValuef() , ak Wrapf() . Men yon egzanp aplikasyon bonè nou an:

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


Ekspòte varyab erè

Chak pake nan baz kod nou an defini pwòp varyab erè li yo, souvan ak estil enkonsistan.

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


Tcheke erè ak errors.Is() ak vlope ak kontèks adisyonèl

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

Sa a te ede pwopaje erè ak plis detay, men souvan sa te lakòz verite, kopi, ak mwens klè nan mòso bwa:

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


Defini erè ekstèn ak Protobuf

Pou API ki fè fas a ekstèn, nou te adopte yon modèl erè ki baze sou Protobuf ki enspire paGraph API Meta a :

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

Apwòch sa a te ede estrikti erè, men sou tan, kalite erè ak kòd yo te ajoute san yon plan klè, ki mennen nan enkonsistans ak repetisyon.


Ak pwoblèm yo te grandi sou tan

Erè yo te deklare toupatou

  • Chak pake defini konstan erè pwòp li yo ki pa gen okenn sistèm santralize.
  • Konstan ak mesaj yo te gaye atravè kodbaz la, ki fè li klè ki erè yon fonksyon ta ka retounen - ugh, èske se gorm.ErrRecordNotFound oswa user.ErrNotFound oswa toude?


Anbalaj erè o aza te mennen nan mòso bwa enkonsistan ak abitrè

  • Anpil fonksyon vlope erè ak mesaj abitrè, enkonsistan san yo pa deklare pwòp kalite erè yo.
  • Jounal yo te pwolib, redondants, ak difisil pou chèche oswa kontwole.
  • Mesaj erè yo te jenerik e souvan yo pa t eksplike sa ki mal oswa ki jan sa te pase. Epitou frajil ak tandans fè chanjman inapèsi.
 unexpected gorm error: failed to find business channel: error received when invoking API: unexpected: context canceled


Pa gen normalisation ki te mennen nan move manyen erè

  • Chak pake jere erè yon fason diferan, sa ki fè li difisil pou konnen si yon fonksyon retounen, vlope, oswa transfòme erè.
  • Kontèks te souvan pèdi kòm erè pwopagasyon.
  • Kouch anwo yo te resevwa vag 500 Erè Sèvè Entèn san kòz rasin klè.


Pa gen kategori ki te fè siveyans enposib

  • Erè yo pa t klase dapre severite oswa konpòtman: Yon context.Canceled Erè anile ka yon konpòtman nòmal lè itilizatè a fèmen tab navigatè a, men li enpòtan si demann lan anile paske demann sa a se owaza dousman.
  • Pwoblèm enpòtan yo te antere anba mòso bwa ki fè bwi, sa ki fè yo difisil pou idantifye.
  • San yo pa kategorize, li te enposib kontwole frekans erè, severite, oswa enpak efektivman.

Li lè yo santralize manyen erè

Retounen nan tablo desen an

Pou adrese defi k ap grandi yo, nou deside bati yon pi bon estrateji erè alantou lide debaz kòd erè santralize ak estriktire .

  • Erè yo deklare tout kote → Santralize deklarasyon erè nan yon sèl kote pou pi bon òganizasyon ak trasabilite.
  • Jounal enkonsistan ak abitrè → Kòd erè estriktire ak fòma klè ak konsistan.
  • Move manyen erè → Standardize kreyasyon erè ak tcheke sou nouvo tip Error a ak yon seri asistan konplè.
  • Pa gen kategorizasyon → Kategorize kòd erè ak tags pou siveyans efikas atravè mòso bwa ak mezi.

Desizyon konsepsyon

Tout kòd erè yo defini nan yon plas santralize ak estrikti namespace.

Sèvi ak espas non pou kreye kòd erè ki klè, ki gen sans, epi ki ka pwolonje. Egzanp:

  • PRFL.USR.NOT_FOUND pou "Itilizatè pa jwenn."
  • FLD.NOT_FOUND pou "Dokiman Flow pa jwenn."
  • Tou de ka pataje yon kòd baz ki kache DEPS.PG.NOT_FOUND , sa vle di "Dosye pa jwenn nan PostgreSQL."


Chak kouch sèvis oswa bibliyotèk dwe sèlman retounen pwòp kòd espas non pa yo .

  • Chak kouch sèvis, depo, oswa bibliyotèk deklare pwòp seri kòd erè pa yo.
  • Lè yon kouch resevwa yon erè nan men yon depandans, li dwe vlope li ak pwòp kòd espas non li anvan li retounen li.
  • Pa egzanp: Lè w ap resevwa yon erè gorm.ErrRecordNotFound soti nan yon depandans, pake "database" la dwe vlope li kòm DEPS.PG.NOT_FOUND . Apre sa, sèvis "pwofil/itilizatè" la dwe vlope li ankò kòm PRFL.USR.NOT_FOUND .


Tout erè dwe aplike koòdone Error .

  • Sa kreye yon fwontyè klè ant erè ki soti nan bibliyotèk twazyèm pati ( error ) ak Error entèn nou an.
  • Sa a ede tou pou pwogrè migrasyon, pou separe ant pakè imigre ak pakè ki poko imigre.


Yon erè ka vlope youn oswa plizyè erè. Ansanm, yo fòme yon pye bwa.

 [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]


Toujou mande context.Context . Ka tache kontèks erè a.

  • Anpil fwa nou te wè mòso bwa ak erè otonòm ki pa gen okenn kontèks, pa gen okenn trace_id , epi yo pa gen okenn lide ki kote li soti.
  • Ka tache plis kle/valè nan erè, ki ka itilize nan mòso bwa oswa siveyans.


Lè yo voye erè atravè fwontyè sèvis yo, se sèlman kòd erè nan nivo siperyè ki ekspoze.

  • Moun ki rele yo pa bezwen wè detay aplikasyon entèn sèvis sa a.


Pou erè ekstèn, kontinye itilize Protobuf ErrorCode ak ErrorType aktyèl la.

  • Sa a asire konpatibilite bak, kidonk kliyan nou yo pa bezwen reekri kòd yo.


Automap kòd erè espas non yo nan kòd Protobuf, kòd estati HTTP, ak tags.

  • Enjenyè yo defini kat jeyografik la nan plas santralize a, epi fondasyon an pral kat chak kòd erè nan korespondan Protobuf ErrorCode , ErrorType , estati gRPC, estati HTTP, ak tags pou antre/mestik.
  • Sa a asire konsistans ak diminye repetisyon.

Fondasyon erè espas non an

Pakè debaz ak kalite

Gen kèk pakè debaz ki fòme fondasyon nouvo fondasyon nou an pou jere erè.

connectly.ai/go/pkgs/

  • errors : Pake prensipal la ki defini kalite Error ak kòd.
  • errors/api : Pou voye erè nan API devan-end oswa ekstèn.
  • errors/E : Pake asistan ki fèt pou itilize ak enpòte pwen.
  • testing : Tès sèvis piblik pou travay ak erè espas non.


Error ak Code

Koòdone Error a se yon ekstansyon koòdone error estanda a, ak metòd adisyonèl pou retounen yon Code . Yon Code aplike kòm yon uint16 .

 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 { /* ... */ }


Pake errors/E ekspòte tout kòd erè ak kalite komen

 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) { /* ... */ }

Egzanp itilizasyon

Egzanp kòd erè:

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


database done pake:

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


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


Pake 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") } // ... }

Oke, gen yon anpil nan nouvo fonksyon ak konsèp nan kòd ki pi wo a. Ann ale nan yo etap pa etap.

Kreye ak anbalaj erè

Premyèman, enpòte errors/E lè l sèvi avèk dot enpòte

Sa a pral pèmèt ou dirèkteman itilize kalite komen tankou Error olye pou yo errors.Error ak aksè nan kòd pa PRFL.USR.NOT_FOUND olye pou yo errors.PRFL.USR.NOT_FOUND .

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


Kreye nouvo erè lè l sèvi avèk CODE.New()

Sipoze ou resevwa yon demann ki pa valab, ou ka kreye yon nouvo erè pa:

 err := PRFL.USR.INVALID_ARGUMENT.New(ctx, "invalid request")
  • PRFL.USR.INVALID_ARGUMENT se yon Code .
  • Yon Code ekspoze metòd tankou New() oswa Wrap() pou kreye yon nouvo erè.
  • Fonksyon New() resevwa context.Context kòm premye agiman an, ki te swiv pa mesaj ak agiman opsyonèl.


Enprime li ak fmt.Print(err) :

 [PRFL.USR.INVALID_ARGUMENT] invalid request


oswa ak fmt.Printf("%+v") pou wè plis detay:

 [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


Vlope yon erè nan yon nouvo erè lè l sèvi avèk CODE.Wrap()

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


pral pwodwi pwodiksyon sa a ak fmt.Print(usrErr) :

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


oswa avèk 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


Stacktrace a ap soti nan Error ki pi anndan an. Si w ap ekri yon fonksyon asistan, ou ka itilize CallerSkip(skip) pou sote ankadreman:

 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, "...") } }

Ajoute kontèks nan erè

Ajoute kontèks nan yon erè lè l sèvi With()

  • Ou ka ajoute pè kle/valè adisyonèl nan erè pa .With(l.String(...)) .
  • logging/l se yon pake asistan pou ekspòte fonksyon sik pou antre.
  • l.String("flag", flag) retounen yon Tag{String: flag} ak l.UUID("user_id, userID) return Tag{Stringer: userID} .
 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")


Tag yo ka soti ak 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


Ajoute kontèks nan erè dirèkteman andedan New() , Wrap() , oswa MapError() :

Pa ogmante fonksyon l.String() ak fanmi li yo, New() ak fonksyon menm jan an ka entelijan detekte tags nan mitan agiman fòma. Pa bezwen prezante diferan fonksyon.

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


pral pwodiksyon:

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

Diferan kalite: Error0 , VlError , ApiError

Kounye a, gen 3 kalite ki aplike interfaces Error yo. Ou ka ajoute plis kalite si sa nesesè. Chak moun ka gen diferan estrikti, ak metòd koutim pou bezwen espesifik.


Error se yon ekstansyon koòdone error estanda Go a

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


Li gen yon metòd prive pou asire ke nou pa aksidantèlman aplike nouvo kalite Error andeyò pake errors a. Nou ka (oswa pa ka) leve restriksyon sa a nan lavni lè nou fè eksperyans ak plis modèl itilizasyon.


Poukisa nou pa jis itilize koòdone error estanda a epi sèvi ak kalite afimasyon?

Paske nou vle separe ant erè twazyèm pati ak erè entèn nou yo. Tout kouch ak pakè nan kòd entèn nou yo dwe toujou retounen Error . Fason sa a nou ka konnen san danje lè nou gen konvèti erè twazyèm pati, ak lè nou sèlman bezwen fè fas ak kòd erè entèn nou an.


Li kreye tou yon fwontyè ant pakè imigre ak pakè ki poko imigre. Retounen nan reyalite, nou pa ka jis deklare yon nouvo kalite, balanse yon ralonj majik, chichote yon èd memwa , ak Lè sa a, tout milyon liy kòd yo konvèti majik epi travay san pwoblèm san okenn pinèz! Non, avni sa a poko la. Li ka vini yon jou, men pou kounye a, nou toujou oblije imigre pakè nou yo youn pa youn.

Error0 se kalite Error default


Pifò kòd erè pral pwodwi yon valè Error0 . Li gen yon base ak yon sub-erè si ou vle. Ou ka itilize NewX() pou retounen yon estrikti konkrè *Error0 olye de yon koòdone Error , men ou bezwen fè atansyon .

 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 se estrikti komen ki pataje tout aplikasyon Error , pou bay fonksyonalite komen: Code() , Message() , StackTrace() , Fields() , ak plis ankò.


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


VlError se pou erè validation

Li ka genyen plizyè sub-erè, epi li bay bèl metòd pou travay avèk moun k ap ede validation.

 type VlError struct { base errs []error }


Ou ka kreye yon VlError menm jan ak lòt Error :

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


Oswa fè yon VlBuilder , ajoute erè nan li, Lè sa a, konvèti li nan yon 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)


Epi enkli pè kle/valè kòm dabitid:

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


Sèvi ak fmt.Printf("%+v", vlErr) pral pwodiksyon:

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


ApiError se yon adaptè pou migre erè API

Précédemment, nou te itilize yon api.Error struct separe pou retounen erè API nan kliyan devan-end ak ekstèn yo. Li gen ladan ErrorType kòm ErrorCode jan mansyone anvan .

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


Kalite sa a kounye a depreche. Olye de sa, nou pral deklare tout kat yo ( ErrorType , ErrorCode , kòd gRPC, kòd HTTP) nan yon kote santralize, epi konvèti yo nan limit korespondan. Mwen pral diskite sou deklarasyon kòd nan pwochen seksyon an .


Pou fè migrasyon nan nouvo kad erè espas non an, nou te ajoute yon espas non tanporè ZZZ.API_TODO . Chak ErrorCode vin yon kòd ZZZ.API_TODO .

 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


Epi ApiError kreye kòm yon adaptè. Tout fonksyon ki te deja retounen *api.Error yo te chanje pou retounen Error (aplike pa *ApiError ) pito.

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


Lè tout migrasyon an fini, itilizasyon anvan an:

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


ta dwe vin:

 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."))


Remake ke ErrorCode a implicitement sòti nan kòd espas non entèn la. Pa bezwen klèman bay li chak fwa. Men, ki jan yo deklare relasyon ki genyen ant kòd? Li pral eksplike nan pwochen seksyon an.

Deklare nouvo kòd erè

Nan pwen sa a, ou deja konnen ki jan yo kreye nouvo erè nan kòd ki egziste deja. Li lè pou eksplike sou kòd ak kijan pou ajoute yon nouvo.


Yon Code aplike kòm yon valè uint16 , ki gen yon prezantasyon fisèl korespondan.

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


Pou estoke fisèl sa yo, gen yon seri tout CodeDesc ki disponib:

 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 }


Men ki jan kòd yo deklare:

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


Apre ou fin deklare nouvo kòd, ou bezwen kouri script jenerasyon an:

 run gen-errors


Kòd ki te pwodwi a pral sanble sa a:

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


Chak kalite Error gen yon kalite Code korespondan

Janm mande ki jan PRFL.USR.NOT_FOUND.New() kreye yon *Error0 ak PRFL.USR.INVALID_ARGUMENTS.New() kreye yon *VlError ? Se paske yo itilize diferan kalite kòd.


Epi chak kalite Code retounen diferan kalite Error , chak ka gen pwòp metòd siplemantè li yo:

 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, /*...*/ } }


Sèvi ak api-code pou make kòd ki disponib pou API ekstèn yo

  • Kòd erè espas non yo ta dwe itilize anndan an.

  • Pou fè yon kòd disponib pou retounen nan ekstèn HTTP API, ou bezwen make li ak api-code . Valè a se errorpb.ErrorCode ki koresponn lan.

  • Si yon kòd erè pa make ak api-code , li se kòd entèn epi yo pral montre kòm yon jenerik Internal Server Error .

  • Remake PRFL.USR.NOT_FOUND se kòd ekstèn, pandan y ap PRFL.USR.REPO.NOT_FOUND se kòd entèn.


Deklare kat ant ErrorCode , ErrorType , ak kòd gRPC/HTTP nan protobuf lè l sèvi avèk opsyon enum:

 // 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.", }];

Kòd UNEXPECTED ak UNKNOWN

Anjeneral, chak kouch gen 2 kòd jenerik UNEXPECTED ak UNKNOWN . Yo sèvi yon ti kras diferan rezon:

  • Kòd UNEXPECTED yo itilize pou erè ki pa ta dwe janm rive.
  • Kòd UNKNOWN itilize pou erè ki pa klèman jere.

Kat erè nan nouvo kòd

Lè w ap resevwa yon erè ki retounen nan yon fonksyon, ou bezwen jere li: konvèti erè twazyèm pati nan erè espas non entèn ak kòd erè kat soti nan kouch enteryè ak kouch ekstèn.


Konvèti erè twazyèm pati yo nan erè espas non entèn yo

Fason ou jere erè depann de: kisa pakè twazyèm pati a retounen ak sa aplikasyon w lan bezwen. Pa egzanp, lè w ap okipe baz done oswa erè API ekstèn:

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


Sèvi ak moun k ap ede yo pou erè espas non entèn yo

  • IsErrorCode(err, CODES...) : Tcheke si erè a gen youn nan kòd espesifye yo.
  • IsErrorGroup(err, GROUP) : Retounen vre si erè a fè pati gwoup opinyon an.


Modèl itilizasyon tipik:

 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() pou ekri kòd kat pi fasil:

Depi kat erè kòd se yon modèl komen, gen yon èd MapError() pou fè ekri kòd pi vit. Kòd ki pi wo a ka reekri kòm:

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


Ou ka fòmate agiman epi ajoute pè kle/valè kòm dabitid:

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

Tès ak espas non Error s

Tès enpòtan pou nenpòt baz kòd ki grav. Fondasyon an bay èd espesyalize tankou ΩxError() pou fè ekri ak afime kondisyon erè nan tès yo pi fasil ak plis ekspresyon.

 // 👉 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")


Gen anpil lòt metòd, epi ou ka chenn yo tou:

 Ω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")


Poukisa itilize metòd olye de Ω(err).To(testing.MatchCode()) ?

Paske metòd yo pi dekouvri. Lè w ap fè fas ak plizyè douzèn fonksyon tankou testing.MatchValues() , li difisil pou konnen kiyès ki pral travay ak Error s ak ki pa pral. Avèk metòd, ou ka tou senpleman tape yon pwen . , epi IDE ou a pral lis tout metòd ki disponib ki fèt espesyalman pou reklame Error s.


Migrasyon

Fondasyon an se jis mwatye nan istwa a. Ekri kòd la? Sa se pati ki fasil. Vrè defi a kòmanse lè ou gen pou pote l nan yon masiv, vivan codebase kote plizyè douzèn enjenyè ap pouse chanjman chak jou, kliyan espere tout bagay pou travay pafètman, ak sistèm jis pa ka sispann kouri.


Migrasyon vini ak responsablite. Li se sou ak anpil atansyon divize cheve ti moso nan kòd, fè ti chanjman nan yon moman, kraze yon tòn tès nan pwosesis la. Lè sa a, manyèlman enspekte ak repare yo youn pa youn, fusion nan branch prensipal la, deplwaye nan pwodiksyon, gade mòso bwa yo ak alèt. Repete li ankò e ankò...


Men kèk konsèy pou migrasyon ke nou te aprann sou wout la:


Kòmanse ak rechèch epi ranplase: Kòmanse pa ranplase ansyen modèl yo ak nouvo kad la. Ranje nenpòt pwoblèm konpilasyon ki rive nan pwosesis sa a.

Pou egzanp, ranplase tout error nan pake sa a ak Error .

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

Nouvo kòd la pral sanble sa a:

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


Migre yon pake nan yon moman: Kòmanse ak pakè nivo ki pi ba yo epi travay wout ou. Nan fason sa a, ou ka asire ke pakè nivo ki pi ba yo konplètman imigre anvan ou ale nan pakè ki pi wo yo.


Ajoute tès inite ki manke yo: Si pati nan kodbaz la manke tès, ajoute yo. Si ou pa gen konfyans nan chanjman ou yo, ajoute plis tès. Yo itil pou asire w ke chanjman ou yo pa kraze fonksyonalite ki egziste deja.


Si pakè w la depann sou apèl pakè ki pi wo yo: Konsidere chanje fonksyon ki gen rapò ak DEPRECATED epi ajoute nouvo fonksyon ak nouvo tip Error .


Sipoze ke w ap imigre pake baz done a, ki gen metòd Transaction() la:

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


Epi li itilize nan pake sèvis itilizatè a:

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


Depi w ap emigre pake database a an premye, kite user a ak plizyè douzèn lòt pakè kòm li. Rele s.repo.CreateUser() la toujou retounen ansyen kalite error pandan metòd Transaction() la bezwen retounen nouvo kalite Error a. Ou ka chanje metòd Transaction() an DEPRECATED epi ajoute yon nouvo metòd TransactionV2() :

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


Ajoute nouvo kòd erè pandan w ap ale : Lè w rankontre yon erè ki pa anfòm nan sa ki deja egziste yo, ajoute yon nouvo kòd. Sa ap ede ou bati yon seri konplè nan kòd erè sou tan. Kòd ki soti nan lòt pakè yo toujou disponib kòm referans.


Konklizyon

Jere erè nan Go ka santi yo senp okòmansman—jis retounen yon error epi kontinye. Men, kòm baz kòd nou an te grandi, senplisite sa a te tounen yon dezòd anmele nan mòso bwa vag, manyen enkonsistan, ak sesyon debogaj kontinuèl.


Lè nou fè bak epi repanse fason nou jere erè, nou te konstwi yon sistèm ki travay pou nou, pa kont nou. Kòd espas non santralize ak estriktire ban nou klè, pandan ke zouti pou kat, anbalaj, ak tès erè fè lavi nou pi fasil. Olye pou n naje nan lanmè mòso bwa, kounye a nou gen erè siyifikatif, trasable ki di nou sa ki mal ak ki kote yo gade.


Fondasyon sa a se pa sèlman sou fè kòd nou an pi pwòp; li se sou ekonomize tan, diminye fristrasyon, epi ede nou prepare pou enkoni. Se jis kòmansman yon vwayaj - nou toujou ap dekouvri plis modèl - men rezilta a se yon sistèm ki ka yon jan kanmenm pote lapè nan manyen erè. Nou swete ke li ka pwovoke kèk ide pou pwojè ou yo tou! 😊



Otè

Mwen se Oliver Nguyen. Yon maker lojisyèl k ap travay sitou nan Go ak JavaScript. Mwen renmen aprann ak wè yon pi bon vèsyon nan tèt mwen chak jou. Okazyonèlman vire sou nouvo pwojè sous louvri. Pataje konesans ak panse pandan vwayaj mwen an.

Pòs la pibliye tou sou blog.connectly.ai ak olivernguyen.io 👋