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 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è.
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è:
errors.New()
ak fmt.Errorf()
pou jenere erè senp.fmt.Errorf()
ak vèb %w
la.errors.Join()
fusion plizyè erè nan yon sèl.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:
errors.New()
oswa fmt.Errorf()
.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.
Erè yo te deklare toupatou
gorm.ErrRecordNotFound
oswa user.ErrNotFound
oswa toude?
Anbalaj erè o aza te mennen nan mòso bwa enkonsistan ak abitrè
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è
Pa gen kategori ki te fè siveyans enposib
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.Pou adrese defi k ap grandi yo, nou deside bati yon pi bon estrateji erè alantou lide debaz kòd erè santralize ak estriktire .
Error
a ak yon seri asistan konplè.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."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 .
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
.
error
) ak Error
entèn nou an.
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.
trace_id
, epi yo pa gen okenn lide ki kote li soti.
Lè yo voye erè atravè fwontyè sèvis yo, se sèlman kòd erè nan nivo siperyè ki ekspoze.
Pou erè ekstèn, kontinye itilize Protobuf ErrorCode ak ErrorType aktyèl la.
Automap kòd erè espas non yo nan kòd Protobuf, kòd estati HTTP, ak tags.
ErrorCode
, ErrorType
, estati gRPC, estati HTTP, ak tags pou antre/mestik.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 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.
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
.Code
ekspoze metòd tankou New()
oswa Wrap()
pou kreye yon nouvo erè.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 yon erè lè l sèvi With()
.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}
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.
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:
UNEXPECTED
yo itilize pou erè ki pa ta dwe janm rive.UNKNOWN
itilize pou erè ki pa klèman jere.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))
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.
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.
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! 😊
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 👋