ການຈັດການຄວາມຜິດພາດໃນ Go ແມ່ນງ່າຍດາຍ ແລະປ່ຽນແປງໄດ້ – ແຕ່ບໍ່ມີໂຄງສ້າງ!
ມັນຄວນຈະງ່າຍດາຍ, ບໍ່ແມ່ນບໍ? ພຽງແຕ່ສົ່ງຄືນ error
, ຫໍ່ດ້ວຍຂໍ້ຄວາມ, ແລະກ້າວຕໍ່ໄປ. ແລ້ວ, ຄວາມລຽບງ່າຍນັ້ນປ່ຽນໄປເປັນຄວາມວຸ້ນວາຍຢ່າງໄວວາ ເນື່ອງຈາກຖານລະຫັດຂອງພວກເຮົາເຕີບໃຫຍ່ຂຶ້ນດ້ວຍແພັກເກັດຕ່າງໆ, ຜູ້ພັດທະນາຫຼາຍຂຶ້ນ, ແລະ "ການແກ້ໄຂດ່ວນ" ທີ່ຄົງຢູ່ຕະຫຼອດໄປ. ເມື່ອເວລາຜ່ານໄປ, ບັນທຶກທີ່ເຕັມໄປດ້ວຍ "ລົ້ມເຫລວ" ແລະ "ບໍ່ຄາດຄິດ", ແລະບໍ່ມີໃຜຮູ້ວ່າມັນເປັນຄວາມຜິດຂອງຜູ້ໃຊ້, ຄວາມຜິດຂອງເຄື່ອງແມ່ຂ່າຍ, ລະຫັດ buggy, ຫຼືມັນເປັນພຽງແຕ່ misalignment ຂອງດາວ!
ຂໍ້ຜິດພາດຖືກສ້າງຂື້ນດ້ວຍຂໍ້ຄວາມທີ່ບໍ່ສອດຄ່ອງກັນ. ແຕ່ລະແພັກເກັດມີຮູບແບບຂອງຕົນເອງ, ຄ່າຄົງທີ່, ຫຼືປະເພດຄວາມຜິດພາດທີ່ກຳນົດເອງ. ລະຫັດຂໍ້ຜິດພາດຈະຖືກເພີ່ມໂດຍຕົນເອງ. ບໍ່ມີວິທີງ່າຍໆທີ່ຈະບອກວ່າຄວາມຜິດພາດທີ່ອາດຈະຖືກສົ່ງຄືນຈາກຫນ້າທີ່ໃດໂດຍບໍ່ມີການຂຸດຄົ້ນເຂົ້າໄປໃນການປະຕິບັດຂອງມັນ!
ດັ່ງນັ້ນ, ຂ້າພະເຈົ້າໄດ້ເອົາສິ່ງທ້າທາຍໃນການສ້າງກອບຄວາມຜິດພາດໃຫມ່. ພວກເຮົາໄດ້ຕັດສິນໃຈທີ່ຈະໄປກັບໂຄງສ້າງ, ລະບົບການສູນກາງການນໍາໃຊ້ລະຫັດ namespace ເພື່ອເຮັດໃຫ້ຄວາມຜິດພາດທີ່ມີຄວາມຫມາຍ, traceable, ແລະ - ທີ່ສໍາຄັນທີ່ສຸດ - ໃຫ້ພວກເຮົາຄວາມສະຫງົບຂອງຈິດໃຈ!
ນີ້ແມ່ນເລື່ອງຂອງວິທີທີ່ພວກເຮົາເລີ່ມຕົ້ນດ້ວຍວິທີການຈັດການຄວາມຜິດພາດທີ່ງ່າຍດາຍ, ໄດ້ຮັບການອຸກອັ່ງຢ່າງລະອຽດຍ້ອນວ່າບັນຫາເພີ່ມຂຶ້ນ, ແລະໃນທີ່ສຸດກໍ່ສ້າງກອບຄວາມຜິດພາດຂອງພວກເຮົາເອງ. ການຕັດສິນໃຈໃນການອອກແບບ, ວິທີການປະຕິບັດ, ບົດຮຽນທີ່ຖອດຖອນໄດ້, ແລະເປັນຫຍັງມັນຫັນປ່ຽນວິທີການຂອງພວກເຮົາໃນການຄຸ້ມຄອງຄວາມຜິດພາດ. ຂ້າພະເຈົ້າຫວັງວ່າມັນຈະນໍາເອົາແນວຄວາມຄິດບາງຢ່າງສໍາລັບທ່ານເຊັ່ນກັນ!
Go ມີວິທີທີ່ກົງໄປກົງມາໃນການຈັດການຄວາມຜິດພາດ: ຄວາມຜິດພາດແມ່ນພຽງແຕ່ຄ່າ. ຄວາມຜິດພາດເປັນພຽງແຕ່ຄ່າທີ່ປະຕິບັດການໂຕ້ຕອບ error
ທີ່ມີວິທີການດຽວ Error() string
. ແທນທີ່ຈະຖິ້ມຂໍ້ຍົກເວັ້ນແລະລົບກວນການປະຕິບັດໃນປັດຈຸບັນ, ຫນ້າທີ່ Go ສົ່ງຄືນຄ່າ error
ຄຽງຄູ່ກັບຜົນໄດ້ຮັບອື່ນໆ. ຫຼັງຈາກນັ້ນ, ຜູ້ໂທສາມາດຕັດສິນໃຈວິທີການຈັດການກັບມັນ: ກວດເບິ່ງມູນຄ່າຂອງມັນເພື່ອຕັດສິນໃຈ, ຫໍ່ດ້ວຍຂໍ້ຄວາມແລະສະພາບການໃຫມ່, ຫຼືພຽງແຕ່ສົ່ງຄືນຂໍ້ຜິດພາດ, ອອກຈາກເຫດຜົນສໍາລັບຜູ້ໂທພໍ່ແມ່.
ພວກເຮົາສາມາດເຮັດໃຫ້ປະເພດໃດຫນຶ່ງ error
ໂດຍການເພີ່ມ Error() string
ໃສ່ມັນ. ຄວາມຍືດຫຍຸ່ນນີ້ຊ່ວຍໃຫ້ແຕ່ລະຊຸດສາມາດກໍານົດຍຸດທະສາດການຈັດການຄວາມຜິດພາດຂອງຕົນເອງ, ແລະເລືອກອັນໃດກໍ່ຕາມທີ່ເຮັດວຽກທີ່ດີທີ່ສຸດສໍາລັບພວກເຂົາ. ນີ້ຍັງປະສົມປະສານໄດ້ດີກັບປັດຊະຍາຂອງ Go ຂອງ composability, ເຮັດໃຫ້ມັນງ່າຍທີ່ຈະຫໍ່, ຂະຫຍາຍ, ຫຼືປັບແຕ່ງຄວາມຜິດພາດຕາມຄວາມຕ້ອງການ.
ການປະຕິບັດທົ່ວໄປແມ່ນການສົ່ງຄືນຄ່າຄວາມຜິດພາດທີ່ປະຕິບັດການໂຕ້ຕອບ error
ແລະໃຫ້ຜູ້ໂທຕັດສິນໃຈວ່າຈະເຮັດແນວໃດຕໍ່ໄປ. ນີ້ແມ່ນຕົວຢ່າງປົກກະຕິ:
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 ໃຫ້ອຸປະກອນຈໍານວນຫນຶ່ງສໍາລັບການເຮັດວຽກກັບຄວາມຜິດພາດ:
errors.New()
ແລະ fmt.Errorf()
ສໍາລັບການສ້າງຄວາມຜິດພາດທີ່ງ່າຍດາຍ.fmt.Errorf()
ແລະ %w
verb.errors.Join()
ລວມຄວາມຜິດພາດຫຼາຍອັນເຂົ້າໄປໃນອັນດຽວ.errors.Is()
ກົງກັບຄວາມຜິດພາດທີ່ມີຄ່າສະເພາະ, errors.As()
ຈັບຄູ່ຂໍ້ຜິດພາດກັບປະເພດສະເພາະ, ແລະ errors.Unwrap()
ດຶງຂໍ້ມູນຄວາມຜິດພາດທີ່ຕິດພັນມາ.
ໃນທາງປະຕິບັດ, ພວກເຮົາມັກຈະເຫັນຮູບແບບເຫຼົ່ານີ້:
errors.New()
ຫຼື fmt.Errorf()
.ໃນຊ່ວງຕົ້ນໆ, ເຊັ່ນດຽວກັບຜູ້ພັດທະນາ Go ຫຼາຍຄົນ, ພວກເຮົາປະຕິບັດຕາມການປະຕິບັດທົ່ວໄປຂອງ Go ແລະຮັກສາຄວາມຜິດພາດໃນການຈັດການຫນ້ອຍທີ່ສຸດແຕ່ມີປະໂຫຍດ. ມັນເຮັດວຽກໄດ້ດີພໍສໍາລັບສອງສາມປີ.
ລວມເອົາ stacktrace ໂດຍໃຊ້ pkg/errors , ເປັນແພັກເກດທີ່ນິຍົມໃນເວລານັ້ນ.
ສົ່ງອອກຄ່າຄົງທີ່ ຫຼືຕົວແປສໍາລັບຂໍ້ຜິດພາດສະເພາະຂອງແພັກເກັດ.
ໃຊ້ errors.Is()
ເພື່ອກວດເບິ່ງຂໍ້ຜິດພາດສະເພາະ.
ຕັດຄວາມຜິດພາດດ້ວຍຂໍ້ຄວາມ ແລະບໍລິບົດໃໝ່.
ສໍາລັບຄວາມຜິດພາດ API, ພວກເຮົາກໍານົດປະເພດຄວາມຜິດພາດແລະລະຫັດທີ່ມີ Protobuf enum.
ລວມທັງ stacktrace ກັບ pkg/errors
ພວກເຮົາໄດ້ໃຊ້ pkg/errors , ເປັນຊຸດການຈັດການຄວາມຜິດພາດທີ່ເປັນທີ່ນິຍົມໃນເວລານັ້ນ, ເພື່ອລວມເອົາ stacktrace ໃນຄວາມຜິດພາດຂອງພວກເຮົາ. ນີ້ແມ່ນເປັນປະໂຫຍດໂດຍສະເພາະສໍາລັບການດີບັກ, ຍ້ອນວ່າມັນອະນຸຍາດໃຫ້ພວກເຮົາຕິດຕາມຕົ້ນກໍາເນີດຂອງຄວາມຜິດພາດໃນທົ່ວພາກສ່ວນຕ່າງໆຂອງຄໍາຮ້ອງສະຫມັກ.
ເພື່ອສ້າງ, ຫໍ່, ແລະເຜີຍແຜ່ຄວາມຜິດພາດດ້ວຍ stacktrace, ພວກເຮົາໄດ້ປະຕິບັດຫນ້າທີ່ເຊັ່ນ: Newf()
, NewValuef()
, ແລະ Wrapf()
. ນີ້ແມ່ນຕົວຢ່າງຂອງການຈັດຕັ້ງປະຕິບັດເບື້ອງຕົ້ນຂອງພວກເຮົາ:
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, } }
ການສົ່ງອອກຕົວແປຄວາມຜິດພາດ
ແຕ່ລະຊຸດໃນ codebase ຂອງພວກເຮົາກໍານົດຕົວແປຄວາມຜິດພາດຂອງຕົນເອງ, ມັກຈະມີຮູບແບບທີ່ບໍ່ສອດຄ່ອງກັນ.
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")
ການກວດສອບຄວາມຜິດພາດທີ່ມີ errors.Is()
ແລະຫໍ່ດ້ວຍບໍລິບົດເພີ່ມເຕີມ
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) }
ອັນນີ້ຊ່ວຍເຜີຍແຜ່ຄວາມຜິດພາດທີ່ມີລາຍລະອຽດເພີ່ມເຕີມ ແຕ່ມັກຈະເຮັດໃຫ້ເກີດຄວາມຄ່ອງແຄ້ວ, ຊໍ້າຊ້ອນ ແລະມີຄວາມຊັດເຈນໜ້ອຍລົງໃນບັນທຶກ:
internal server error: failed to query user: user not found (id=52a0a433-3922-48bd-a7ac-35dd8972dfe5): record not found: not found
ການກໍານົດຄວາມຜິດພາດພາຍນອກກັບ Protobuf
ສໍາລັບ APIs ພາຍນອກ, ພວກເຮົາໄດ້ຮັບຮອງເອົາຮູບແບບຄວາມຜິດພາດທີ່ອີງໃສ່ Protobuf ທີ່ໄດ້ຮັບການດົນໃຈໂດຍMeta's 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; }
ວິທີການນີ້ຊ່ວຍໃຫ້ໂຄງສ້າງຜິດພາດ, ແຕ່ເມື່ອເວລາຜ່ານໄປ, ປະເພດຂໍ້ຜິດພາດແລະລະຫັດໄດ້ຖືກເພີ່ມໂດຍບໍ່ມີແຜນການທີ່ຊັດເຈນ, ນໍາໄປສູ່ຄວາມບໍ່ສອດຄ່ອງແລະການຊໍ້າຊ້ອນ.
ຂໍ້ຜິດພາດໄດ້ຖືກປະກາດຢູ່ທົ່ວທຸກແຫ່ງ
gorm.ErrRecordNotFound
ຫຼື user.ErrNotFound
ຫຼືທັງສອງ?
ການຫໍ່ຄວາມຜິດພາດແບບສຸ່ມເຮັດໃຫ້ບັນທຶກທີ່ບໍ່ສອດຄ່ອງກັນ ແລະ arbitrary
unexpected gorm error: failed to find business channel: error received when invoking API: unexpected: context canceled
ບໍ່ມີມາດຕະຖານເຮັດໃຫ້ການຈັດການຄວາມຜິດພາດທີ່ບໍ່ຖືກຕ້ອງ
ບໍ່ມີການຈັດປະເພດເຮັດໃຫ້ການຕິດຕາມເປັນໄປບໍ່ໄດ້
context.Canceled
ຄວາມຜິດພາດທີ່ຖືກຍົກເລີກອາດຈະເປັນພຶດຕິກໍາປົກກະຕິເມື່ອຜູ້ໃຊ້ປິດແຖບບຣາວເຊີ, ແຕ່ມັນສໍາຄັນຖ້າຄໍາຮ້ອງຂໍຖືກຍົກເລີກເນື່ອງຈາກການສອບຖາມນັ້ນຊ້າແບບສຸ່ມ.ເພື່ອແກ້ໄຂສິ່ງທ້າທາຍທີ່ເພີ່ມຂຶ້ນ, ພວກເຮົາໄດ້ຕັດສິນໃຈສ້າງຍຸດທະສາດຄວາມຜິດພາດທີ່ດີຂຶ້ນປະມານແນວຄວາມຄິດຫຼັກຂອງ ລະຫັດຄວາມຜິດພາດທີ່ລວມສູນແລະໂຄງສ້າງ .
Error
ໃຫມ່ ທີ່ມີຊຸດຂອງຕົວຊ່ວຍທີ່ສົມບູນແບບ.ລະຫັດຂໍ້ຜິດພາດທັງຫມົດແມ່ນຖືກກໍານົດຢູ່ໃນຈຸດສູນກາງທີ່ມີໂຄງສ້າງ namespace.
ໃຊ້ namespaces ເພື່ອສ້າງລະຫັດຄວາມຜິດພາດທີ່ຊັດເຈນ, ມີຄວາມຫມາຍ, ແລະຂະຫຍາຍໄດ້. ຕົວຢ່າງ:
PRFL.USR.NOT_FOUND
ສໍາລັບ "ບໍ່ພົບຜູ້ໃຊ້."FLD.NOT_FOUND
ສໍາລັບ "ບໍ່ພົບເອກະສານ Flow."DEPS.PG.NOT_FOUND
, ຊຶ່ງຫມາຍຄວາມວ່າ "ບໍ່ພົບບັນທຶກໃນ PostgreSQL."
ແຕ່ລະຊັ້ນການບໍລິການ ຫຼືຫ້ອງສະໝຸດຈະຕ້ອງສົ່ງຄືນລະຫັດ namespace ຂອງຕົນເອງເທົ່ານັ້ນ .
gorm.ErrRecordNotFound
ຈາກການຂຶ້ນກັບ, ຊຸດ "ຖານຂໍ້ມູນ" ຕ້ອງຫໍ່ມັນເປັນ DEPS.PG.NOT_FOUND
. ຕໍ່ມາ, ການບໍລິການ "ໂປຣໄຟລ໌/ຜູ້ໃຊ້" ຈະຕ້ອງຫໍ່ມັນອີກຄັ້ງເປັນ PRFL.USR.NOT_FOUND
.
ຄວາມຜິດພາດທັງໝົດຕ້ອງປະຕິບັດ ການໂຕ້ຕອບ Error
.
error
) ແລະ Error
ພາຍໃນຂອງພວກເຮົາ s.
ຂໍ້ຜິດພາດສາມາດຫໍ່ຫນຶ່ງຫຼືຫຼາຍຂໍ້ຜິດພາດ. ຮ່ວມກັນ, ພວກເຂົາປະກອບເປັນຕົ້ນໄມ້.
[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]
ຕ້ອງການ context.Context
ສະເໝີ . ສາມາດແນບບໍລິບົດກັບຄວາມຜິດພາດໄດ້.
trace_id
, ແລະບໍ່ຮູ້ວ່າມັນມາຈາກໃສ.
ເມື່ອຂໍ້ຜິດພາດຖືກສົ່ງຜ່ານຂອບເຂດການບໍລິການ, ພຽງແຕ່ລະຫັດຂໍ້ຜິດພາດລະດັບສູງສຸດທີ່ຖືກເປີດເຜີຍ.
ສໍາລັບຄວາມຜິດພາດພາຍນອກ, ສືບຕໍ່ໃຊ້ Protobuf ErrorCode ແລະ ErrorType ໃນປັດຈຸບັນ.
Automap ລະຫັດຄວາມຜິດພາດ namespace ກັບລະຫັດ Protobuf, ລະຫັດສະຖານະ HTTP, ແລະ tags.
ErrorCode
, ErrorType
, gRPC status, HTTP status, ແລະ tags ສໍາລັບການບັນທຶກ / metrics.ມີຊຸດຫຼັກຈຳນວນໜຶ່ງທີ່ເປັນພື້ນຖານຂອງກອບການຈັດການກັບຄວາມຜິດພາດອັນໃໝ່ຂອງພວກເຮົາ.
connectly.ai/go/pkgs/
errors
: ຊຸດຫຼັກທີ່ກໍານົດປະເພດ Error
ແລະລະຫັດ.errors/api
: ສໍາລັບການສົ່ງຂໍ້ຜິດພາດໄປຫາ Front-end ຫຼື API ພາຍນອກ.errors/E
: ຊຸດຕົວຊ່ວຍທີ່ມີຈຸດປະສົງເພື່ອໃຊ້ກັບການນໍາເຂົ້າຈຸດ.testing
: ການທົດສອບອຸປະກອນສໍາລັບການເຮັດວຽກທີ່ມີຄວາມຜິດພາດ namespace.
Error
ແລະ Code
ການໂຕ້ຕອບ Error
ແມ່ນສ່ວນຂະຫຍາຍຂອງການໂຕ້ຕອບ error
ມາດຕະຖານ, ມີວິທີການເພີ່ມເຕີມເພື່ອສົ່ງຄືນ Code
. Code
ຖືກປະຕິບັດເປັນ 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 { /* ... */ }
Package errors/E
ສົ່ງອອກລະຫັດຂໍ້ຜິດພາດທັງໝົດ ແລະປະເພດທົ່ວໄປ
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) { /* ... */ }
ຕົວຢ່າງລະຫັດຂໍ້ຜິດພາດ:
// 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
ຊຸດ:
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)) } }
ແພັກເກດ 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") } // ... }
ດີ, ມີຫຼາຍຫນ້າທີ່ແລະແນວຄວາມຄິດໃຫມ່ໃນລະຫັດຂ້າງເທິງ. ໃຫ້ເຮົາໄປຜ່ານພວກມັນເທື່ອລະກ້າວ.
ທໍາອິດ, ການນໍາເຂົ້າ errors/E
ໂດຍໃຊ້ຈຸດນໍາເຂົ້າ
ອັນນີ້ຈະເຮັດໃຫ້ເຈົ້າສາມາດໃຊ້ປະເພດທົ່ວໄປໄດ້ໂດຍກົງເຊັ່ນ Error
ແທນ errors.Error
ແລະເຂົ້າເຖິງລະຫັດໂດຍ PRFL.USR.NOT_FOUND
ແທນ errors.PRFL.USR.NOT_FOUND
.
import . "connectly.ai/go/pkgs/errors/E"
ສ້າງຂໍ້ຜິດພາດໃຫມ່ໂດຍໃຊ້ CODE.New()
ສົມມຸດວ່າທ່ານໄດ້ຮັບຄໍາຮ້ອງຂໍທີ່ບໍ່ຖືກຕ້ອງ, ທ່ານສາມາດສ້າງຂໍ້ຜິດພາດໃຫມ່ໂດຍ:
err := PRFL.USR.INVALID_ARGUMENT.New(ctx, "invalid request")
PRFL.USR.INVALID_ARGUMENT
ເປັນ Code
.Code
ເປີດເຜີຍວິທີການເຊັ່ນ New()
ຫຼື Wrap()
ເພື່ອສ້າງຂໍ້ຜິດພາດໃຫມ່.New()
ໄດ້ຮັບ context.Context
ເປັນ argument ທໍາອິດ, ຕາມດ້ວຍຂໍ້ຄວາມ ແລະ argument ທາງເລືອກ.
ພິມດ້ວຍ fmt.Print(err)
:
[PRFL.USR.INVALID_ARGUMENT] invalid request
ຫຼືດ້ວຍ fmt.Printf("%+v")
ເພື່ອເບິ່ງລາຍລະອຽດເພີ່ມເຕີມ:
[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
ຫໍ່ຂໍ້ຜິດພາດພາຍໃນຂໍ້ຜິດພາດໃຫມ່ໂດຍໃຊ້ CODE.Wrap()
dbErr := DEPS.PG.NOT_FOUND.Wrap(ctx, gorm.ErrRecordNotFound, "not found") usrErr := PRFL.USR.NOT_FOUND.Wrap(ctx, dbErr, "user not found")
ຈະຜະລິດຜົນຜະລິດນີ້ດ້ວຍ fmt.Print(usrErr)
:
[PRFL.USR.NOT_FOUND] user not found → [DEPS.PG.NOT_FOUND] not found → record not found
ຫຼືດ້ວຍ 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 ຈະມາຈາກ Error
ພາຍໃນທີ່ສຸດ . ຖ້າຫາກວ່າທ່ານກໍາລັງຂຽນຫນ້າທີ່ການຊ່ວຍເຫຼືອ, ທ່ານສາມາດໃຊ້ CallerSkip(skip)
ເພື່ອຂ້າມເຟຣມ:
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, "...") } }
ເພີ່ມບໍລິບົດຕໍ່ກັບຄວາມຜິດພາດໂດຍໃຊ້ With()
.With(l.String(...))
.logging/l
ເປັນຊຸດຕົວຊ່ວຍໃນການສົ່ງອອກໜ້າຕານ້ໍາຕານສຳລັບການຕັດໄມ້.l.String("flag", flag)
ສົ່ງຄືນ Tag{String: flag}
ແລະ l.UUID("user_id, userID)
ຕອບ 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")
ແທັກສາມາດອອກໄດ້ດ້ວຍ 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
ເພີ່ມບໍລິບົດຕໍ່ກັບຄວາມຜິດພາດໂດຍກົງພາຍໃນ New()
, Wrap()
, ຫຼື MapError()
:
ໂດຍການໃຊ້ຟັງຊັນ l.String()
ແລະຄອບຄົວຂອງມັນ, New()
ແລະຟັງຊັນທີ່ຄ້າຍຄືກັນສາມາດກວດຫາ tags ໃນລະຫວ່າງການຈັດຮູບແບບໄດ້ຢ່າງສະຫຼາດ. ບໍ່ຈໍາເປັນຕ້ອງແນະນໍາຫນ້າທີ່ທີ່ແຕກຕ່າງກັນ.
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), )
ຈະອອກ:
[INF.HEALTH.NOT_READY] service "magic" is not ready (retried 2 times) {"flag": "ABRW", "count": 2}
Error0
, VlError
, ApiError
ໃນປັດຈຸບັນ, ມີ 3 ປະເພດທີ່ປະຕິບັດການໂຕ້ຕອບ Error
. ທ່ານສາມາດເພີ່ມປະເພດເພີ່ມເຕີມຖ້າຈໍາເປັນ. ແຕ່ລະຄົນສາມາດມີໂຄງສ້າງທີ່ແຕກຕ່າງກັນ, ດ້ວຍວິທີການທີ່ກໍາຫນົດເອງສໍາລັບຄວາມຕ້ອງການສະເພາະ.
Error
ແມ່ນສ່ວນຂະຫຍາຍຂອງ ການໂຕ້ຕອບ error
ມາດຕະຖານຂອງ Go
type Error interface { error Code() Message() Fields() []tags.Field StackTrace() stacktrace.StackTrace _base() *base // a private method }
ມັນປະກອບດ້ວຍວິທີການສ່ວນຕົວເພື່ອຮັບປະກັນວ່າພວກເຮົາບໍ່ໄດ້ປະຕິບັດປະເພດ Error
ໃຫມ່ໂດຍບັງເອີນຢູ່ນອກຊຸດ errors
. ພວກເຮົາອາດຈະ (ຫຼືອາດຈະບໍ່) ຍົກຂໍ້ຈໍາກັດນັ້ນໃນອະນາຄົດເມື່ອພວກເຮົາປະສົບກັບຮູບແບບການນໍາໃຊ້ຫຼາຍຂຶ້ນ.
ເປັນຫຍັງພວກເຮົາບໍ່ພຽງແຕ່ໃຊ້ ການໂຕ້ຕອບ error
ມາດຕະຖານ ແລະໃຊ້ການຢືນຢັນປະເພດ?
ເນື່ອງຈາກວ່າພວກເຮົາຕ້ອງການແຍກລະຫວ່າງຄວາມຜິດພາດຂອງພາກສ່ວນທີສາມແລະຄວາມຜິດພາດພາຍໃນຂອງພວກເຮົາ. ຊັ້ນຂໍ້ມູນ ແລະແພັກເກັດທັງໝົດໃນລະຫັດພາຍໃນຂອງພວກເຮົາຈະຕ້ອງສົ່ງຄືນ Error
ສະເໝີ . ວິທີນີ້ພວກເຮົາສາມາດຮູ້ໄດ້ຢ່າງປອດໄພເມື່ອພວກເຮົາຕ້ອງປ່ຽນຂໍ້ຜິດພາດຂອງພາກສ່ວນທີສາມ, ແລະໃນເວລາທີ່ພວກເຮົາຕ້ອງການພຽງແຕ່ຈັດການກັບລະຫັດຂໍ້ຜິດພາດພາຍໃນຂອງພວກເຮົາ.
ມັນຍັງສ້າງຂອບເຂດລະຫວ່າງແພັກເກດທີ່ຍ້າຍອອກໄປ ແລະແພັກເກດທີ່ຍັງບໍ່ໄດ້ຍ້າຍ. ກັບຄືນສູ່ຄວາມເປັນຈິງ, ພວກເຮົາບໍ່ສາມາດພຽງແຕ່ປະກາດປະເພດໃໝ່, ໂບກໃບໄມ້ວິເສດ, ກະຊິບການສະ ກົດຄຳເຕືອນ , ແລະຫຼັງຈາກນັ້ນລະຫັດນັບລ້ານເສັ້ນຈະຖືກປ່ຽນຢ່າງມະຫັດສະຈັນ ແລະເຮັດວຽກໄດ້ຢ່າງບໍ່ຢຸດຢັ້ງໂດຍບໍ່ມີແມງໄມ້! ບໍ່, ອະນາຄົດນັ້ນບໍ່ຢູ່ທີ່ນີ້. ມັນອາດຈະມາຮອດມື້ຫນຶ່ງ, ແຕ່ສໍາລັບຕອນນີ້, ພວກເຮົາຍັງຕ້ອງຍ້າຍແພັກເກັດຂອງພວກເຮົາເທື່ອລະອັນ.
Error0
ແມ່ນປະເພດ Error
ພາດເລີ່ມຕົ້ນ
ລະຫັດຂໍ້ຜິດພາດສ່ວນໃຫຍ່ຈະສ້າງ ຄ່າ Error0
. ມັນປະກອບດ້ວຍ base
ແລະຄວາມຜິດພາດຍ່ອຍທາງເລືອກ. ທ່ານສາມາດນໍາໃຊ້ NewX()
ເພື່ອກັບຄືນມາເປັນ *Error0
ໂຄງສ້າງສີມັງແທນທີ່ຈະເປັນການໂຕ້ຕອບ Error
, ແຕ່ທ່ານຈໍາເປັນຕ້ອງ ລະມັດລະວັງ .
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
ແມ່ນໂຄງສ້າງທົ່ວໄປທີ່ແບ່ງປັນໂດຍການປະຕິບັດ Error
ທັງຫມົດ, ເພື່ອສະຫນອງການເຮັດວຽກທົ່ວໄປ: Code()
, Message()
, StackTrace()
, Fields()
, ແລະອື່ນໆອີກ.
type base struct { code Code msg string kv []tags.Field stack stacktrace.StackTrace }
VlError
ແມ່ນສໍາລັບຄວາມຜິດພາດໃນການກວດສອບ
ມັນສາມາດບັນຈຸຂໍ້ຜິດພາດຍ່ອຍຫຼາຍ, ແລະສະຫນອງວິທີການທີ່ດີທີ່ຈະເຮັດວຽກຮ່ວມກັບຜູ້ຊ່ວຍການກວດສອບ.
type VlError struct { base errs []error }
ທ່ານສາມາດສ້າງ VlError
ທີ່ຄ້າຍຄືກັນກັບ Error
ອື່ນໆ:
err := PRFL.USR.INVALID_ARGUMENT.New(ctx, "invalid request")
ຫຼືສ້າງ VlBuilder
, ເພີ່ມຂໍ້ຜິດພາດໃສ່ມັນ, ຈາກນັ້ນປ່ຽນເປັນ 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)
ແລະລວມເອົາຄູ່ຄີ/ຄ່າຕາມປົກກະຕິ:
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))
ການໃຊ້ fmt.Printf("%+v", vlErr)
ຈະສົ່ງຜົນອອກມາ:
[PRFL.USR.INVALID_ARGUMENT] invalid request {"testingenv": true, "user_id": "A1234567890"}
ApiError
ເປັນອະແດັບເຕີສໍາລັບການເຄື່ອນຍ້າຍ API ຜິດພາດ
ກ່ອນຫນ້ານີ້, ພວກເຮົາໄດ້ໃຊ້ໂຄງສ້າງ api.Error
ແຍກຕ່າງຫາກສໍາລັບການສົ່ງຄືນຂໍ້ຜິດພາດ API ກັບລູກຄ້າດ້ານຫນ້າແລະພາຍນອກ. ມັນປະກອບມີ ErrorType
ເປັນ ErrorCode
ດັ່ງທີ່ ໄດ້ກ່າວມາກ່ອນ .
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 // ... }
ປະເພດນີ້ຖືກຍົກເລີກໃນປັດຈຸບັນ. ແທນທີ່ຈະ, ພວກເຮົາຈະປະກາດແຜນທີ່ທັງຫມົດ ( ErrorType
, ErrorCode
, gRPC code , HTTP code ) ໃນສະຖານທີ່ສູນກາງ, ແລະປ່ຽນໃຫ້ເຂົາເຈົ້າຢູ່ໃນຂອບເຂດທີ່ສອດຄ້ອງກັນ. ຂ້ອຍຈະສົນທະນາກ່ຽວກັບການປະກາດລະຫັດໃນ ພາກຕໍ່ໄປ .
ເພື່ອເຮັດການຍ້າຍໄປຫາກອບການຜິດພາດ namespace ໃໝ່, ພວກເຮົາໄດ້ເພີ່ມ namespace ຊົ່ວຄາວ ZZZ.API_TODO
. ທຸກໆ ErrorCode
ກາຍເປັນລະຫັດ 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
ແລະ ApiError
ຖືກສ້າງເປັນອະແດບເຕີ. ຟັງຊັນທັງໝົດທີ່ສົ່ງກັບ *api.Error
ໃນເມື່ອກ່ອນຖືກປ່ຽນເປັນ Error
(ປະຕິບັດໂດຍ *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...) }
ເມື່ອການເຄື່ອນຍ້າຍທັງຫມົດແມ່ນສໍາເລັດ, ການນໍາໃຊ້ທີ່ຜ່ານມາ:
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()
ຄວນຈະກາຍເປັນ:
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."))
ສັງເກດເຫັນວ່າ ErrorCode
ແມ່ນມາຈາກລະຫັດ namespace ພາຍໃນ. ບໍ່ຈໍາເປັນຕ້ອງກໍານົດຢ່າງຈະແຈ້ງທຸກຄັ້ງ. ແຕ່ວິທີການປະກາດຄວາມສໍາພັນລະຫວ່າງລະຫັດ? ມັນຈະຖືກອະທິບາຍໃນພາກຕໍ່ໄປ.
ໃນຈຸດນີ້, ເຈົ້າຮູ້ວິທີການສ້າງຂໍ້ຜິດພາດໃຫມ່ຈາກລະຫັດທີ່ມີຢູ່. ມັນເປັນເວລາທີ່ຈະອະທິບາຍກ່ຽວກັບລະຫັດແລະວິທີການເພີ່ມໃຫມ່.
ລະ Code
ຖືກປະຕິບັດເປັນ ຄ່າ uint16
, ເຊິ່ງມີການນໍາສະເຫນີສະຕຣິງທີ່ສອດຄ້ອງກັນ.
type Code struct { code: uint16 } fmt.Printf("%q", DEPS.PG.NOT_FOUND) // "DEPS.PG.NOT_FOUND"
ເພື່ອເກັບຮັກສາສະຕຣິງເຫຼົ່ານັ້ນ, ມີ array ຂອງ 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 }
ນີ້ແມ່ນວິທີການປະກາດລະຫັດ:
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"` }
ຫຼັງຈາກປະກາດລະຫັດໃຫມ່, ທ່ານຈໍາເປັນຕ້ອງ ແລ່ນ ສະຄິບລຸ້ນ:
run gen-errors
ລະຫັດທີ່ສ້າງຂຶ້ນຈະມີລັກສະນະນີ້:
// 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", })) }
ແຕ່ລະ ປະເພດ Error
ມີ ປະເພດ Code
ທີ່ສອດຄ້ອງກັນ
ເຄີຍສົງໄສວ່າ PRFL.USR.NOT_FOUND.New()
ສ້າງ *Error0
ແລະ PRFL.USR.INVALID_ARGUMENTS.New()
ສ້າງ *VlError
ແນວໃດ? ມັນເປັນຍ້ອນວ່າພວກເຂົາໃຊ້ປະເພດລະຫັດທີ່ແຕກຕ່າງກັນ.
ແລະແຕ່ລະປະເພດ Code
ສົ່ງຄືນປະເພດ Error
ທີ່ແຕກຕ່າງກັນ, ແຕ່ລະຄົນສາມາດມີວິທີການພິເສດຂອງຕົນເອງ:
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, /*...*/ } }
ໃຊ້ api-code
ເພື່ອຫມາຍລະຫັດທີ່ມີຢູ່ສໍາລັບ API ພາຍນອກ
ລະຫັດຂໍ້ຜິດພາດ namespace ຄວນຖືກໃຊ້ພາຍໃນ.
ເພື່ອເຮັດໃຫ້ລະຫັດສາມາດໃຊ້ໄດ້ສໍາລັບການກັບຄືນໃນ HTTP API ພາຍນອກ, ທ່ານຈໍາເປັນຕ້ອງຫມາຍມັນດ້ວຍ api-code
. ຄ່າແມ່ນ errorpb.ErrorCode
ທີ່ສອດຄ້ອງກັນ .
ຖ້າລະຫັດຂໍ້ຜິດພາດບໍ່ໄດ້ຖືກໝາຍດ້ວຍ api-code
, ມັນເປັນລະຫັດພາຍໃນ ແລະຈະຖືກສະແດງເປັນ Internal Server Error
ທົ່ວໄປ .
ສັງເກດເຫັນວ່າ PRFL.USR.NOT_FOUND
ເປັນລະຫັດພາຍນອກ, ໃນຂະນະທີ່ PRFL.USR.REPO.NOT_FOUND
ເປັນລະຫັດພາຍໃນ.
ປະກາດແຜນທີ່ລະຫວ່າງ ErrorCode
, ErrorType
, ແລະລະຫັດ gRPC/HTTP ໃນ protobuf ໂດຍໃຊ້ຕົວເລືອກ 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.", }];
ລະຫັດ UNEXPECTED
ແລະ UNKNOWN
ແຕ່ລະຊັ້ນມັກຈະມີ 2 ລະຫັດທົ່ວໄປ UNEXPECTED
ແລະ UNKNOWN
. ພວກເຂົາຮັບໃຊ້ຈຸດປະສົງທີ່ແຕກຕ່າງກັນເລັກນ້ອຍ:
UNEXPECTED
ຖືກໃຊ້ສໍາລັບຄວາມຜິດພາດທີ່ບໍ່ຄວນເກີດຂຶ້ນ.UNKNOWN
ແມ່ນໃຊ້ສໍາລັບຄວາມຜິດພາດທີ່ບໍ່ໄດ້ຈັດການຢ່າງຈະແຈ້ງ.ເມື່ອໄດ້ຮັບຄວາມຜິດພາດທີ່ສົ່ງຄືນມາຈາກຟັງຊັນ, ທ່ານຈໍາເປັນຕ້ອງຈັດການກັບມັນ: ປ່ຽນຄວາມຜິດພາດຂອງພາກສ່ວນທີສາມເປັນຂໍ້ຜິດພາດຂອງ namespace ພາຍໃນແລະລະຫັດຄວາມຜິດພາດຂອງແຜນທີ່ຈາກຊັ້ນໃນໄປຫາຊັ້ນນອກ.
ປ່ຽນຂໍ້ຜິດພາດຂອງພາກສ່ວນທີສາມເປັນຂໍ້ຜິດພາດຂອງ namespace ພາຍໃນ
ວິທີທີ່ທ່ານຈັດການກັບຄວາມຜິດພາດແມ່ນຂຶ້ນກັບ: ສິ່ງທີ່ຊຸດພາກສ່ວນທີສາມສົ່ງຄືນແລະສິ່ງທີ່ຄໍາຮ້ອງສະຫມັກຂອງທ່ານຕ້ອງການ. ຕົວຢ່າງ, ເມື່ອຈັດການຖານຂໍ້ມູນຫຼື API ພາຍນອກຜິດພາດ:
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") }
ການນໍາໃຊ້ຕົວຊ່ວຍສໍາລັບຄວາມຜິດພາດ namespace ພາຍໃນ
IsErrorCode(err, CODES...)
: ກວດເບິ່ງວ່າຄວາມຜິດພາດມີລະຫັດໃດນຶ່ງທີ່ລະບຸ.IsErrorGroup(err, GROUP)
: ກັບຄືນ true ຖ້າຂໍ້ຜິດພາດເປັນຂອງກຸ່ມການປ້ອນຂໍ້ມູນ.
ຮູບແບບການນໍາໃຊ້ປົກກະຕິ:
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()
ສໍາລັບການຂຽນລະຫັດແຜນທີ່ງ່າຍຂຶ້ນ:
ເນື່ອງຈາກລະຫັດຄວາມຜິດພາດໃນແຜນທີ່ເປັນຮູບແບບທົ່ວໄປ, ມີຕົວຊ່ວຍ MapError()
ເພື່ອເຮັດໃຫ້ການຂຽນລະຫັດໄວຂຶ້ນ. ລະຫັດຂ້າງເທິງສາມາດຂຽນຄືນໄດ້ຄື:
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") }
ທ່ານສາມາດຈັດຮູບແບບການໂຕ້ຖຽງ ແລະເພີ່ມຄູ່ຄີ/ຄ່າໄດ້ຕາມປົກກະຕິ:
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 ການທົດສອບແມ່ນສໍາຄັນສໍາລັບພື້ນຖານລະຫັດທີ່ຮ້າຍແຮງ. ໂຄງຮ່າງການໃຫ້ຜູ້ຊ່ວຍພິເສດເຊັ່ນ ΩxError()
ເພື່ອເຮັດໃຫ້ການຂຽນແລະຢືນຢັນເງື່ອນໄຂຄວາມຜິດພາດໃນການທົດສອບງ່າຍຂຶ້ນແລະສະແດງອອກຫຼາຍ.
// 👉 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")
ມີຫຼາຍວິທີການເພີ່ມເຕີມ, ແລະທ່ານສາມາດຕ່ອງໂສ້ມັນເຊັ່ນດຽວກັນ:
Ω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")
ເປັນຫຍັງຕ້ອງໃຊ້ວິທີການແທນ Ω(err).To(testing.MatchCode())
?
ເນື່ອງຈາກວ່າວິທີການແມ່ນຄົ້ນພົບຫຼາຍ. ໃນເວລາທີ່ທ່ານປະເຊີນກັບຫລາຍສິບຫນ້າທີ່ເຊັ່ນ testing.MatchValues()
, ມັນເປັນການຍາກທີ່ຈະຮູ້ວ່າອັນໃດທີ່ຈະເຮັດວຽກກັບ Error
s ແລະອັນໃດຈະບໍ່. ດ້ວຍວິທີການ, ທ່ານພຽງແຕ່ສາມາດພິມຈຸດ .
, ແລະ IDE ຂອງທ່ານຈະລາຍຊື່ວິທີການທີ່ມີຢູ່ທັງຫມົດທີ່ຖືກອອກແບບໂດຍສະເພາະສໍາລັບການຢືນຢັນ Error
.
ກອບແມ່ນພຽງແຕ່ເຄິ່ງຫນຶ່ງຂອງເລື່ອງ. ຂຽນລະຫັດ? ນັ້ນແມ່ນສ່ວນທີ່ງ່າຍ. ສິ່ງທ້າທາຍທີ່ແທ້ຈິງເລີ່ມຕົ້ນໃນເວລາທີ່ທ່ານຕ້ອງເອົາມັນເຂົ້າໄປໃນ codebase ດໍາລົງຊີວິດຂະຫນາດໃຫຍ່ທີ່ມີວິສະວະກອນຫລາຍສິບຄົນກໍາລັງຊຸກຍູ້ການປ່ຽນແປງປະຈໍາວັນ, ລູກຄ້າຄາດຫວັງວ່າທຸກສິ່ງທຸກຢ່າງຈະເຮັດວຽກຢ່າງສົມບູນ, ແລະລະບົບບໍ່ສາມາດຢຸດການເຮັດວຽກໄດ້.
ການເຄື່ອນຍ້າຍມາດ້ວຍຄວາມຮັບຜິດຊອບ. ມັນກ່ຽວກັບການແຍກລະຫັດ ຜົມ ນ້ອຍໆຢ່າງລະມັດລະວັງ, ປ່ຽນແປງເລັກນ້ອຍໃນແຕ່ລະຄັ້ງ, ທໍາລາຍການທົດສອບຫຼາຍໂຕນໃນຂະບວນການ. ຫຼັງຈາກນັ້ນ, ດ້ວຍຕົນເອງກວດກາແລະແກ້ໄຂໃຫ້ເຂົາເຈົ້າຫນຶ່ງໂດຍຫນຶ່ງ, ໂຮມເຂົ້າໄປໃນສາຂາຕົ້ນຕໍ, deploying ກັບການຜະລິດ, ການສັງເກດເບິ່ງໄມ້ທ່ອນແລະການແຈ້ງເຕືອນ. ເຮັດຊ້ຳໆຊ້ຳໆ...
ນີ້ແມ່ນຄໍາແນະນໍາບາງຢ່າງສໍາລັບການຍົກຍ້າຍທີ່ພວກເຮົາໄດ້ຮຽນຮູ້ຕາມທາງ:
ເລີ່ມຕົ້ນດ້ວຍການຄົ້ນຫາ ແລະປ່ຽນແທນ: ເລີ່ມຕົ້ນດ້ວຍການປ່ຽນຮູບແບບເກົ່າດ້ວຍກອບໃໝ່. ແກ້ໄຂບັນຫາການລວບລວມທີ່ເກີດຂື້ນຈາກຂະບວນການນີ້.
ຕົວຢ່າງ, ແທນທີ່ error
ທັງໝົດໃນຊຸດນີ້ດ້ວຍ Error
.
type ProfileController interface { LoginUser(req *LoginRequest) (*LoginResponse, error) QueryUser(req *QueryUserRequest) (*QueryUserResponse, error) }
ລະຫັດໃຫມ່ຈະມີລັກສະນະນີ້:
import . "connectly.ai/go/pkgs/errors" type ProfileController interface { LoginUser(req *LoginRequest) (*LoginResponse, Error) QueryUser(req *QueryUserRequest) (*QueryUserResponse, Error) }
ຍ້າຍແພັກເກັດໜຶ່ງເທື່ອລະອັນ: ເລີ່ມຕົ້ນດ້ວຍແພັກເກັດລະດັບຕໍ່າສຸດ ແລະເຮັດວຽກຂອງທ່ານຂຶ້ນ. ວິທີນີ້, ທ່ານສາມາດຮັບປະກັນວ່າການຫຸ້ມຫໍ່ລະດັບຕ່ໍາໄດ້ຖືກຍົກຍ້າຍຢ່າງເຕັມສ່ວນກ່ອນທີ່ຈະກ້າວໄປສູ່ລະດັບທີ່ສູງກວ່າ.
ເພີ່ມການທົດສອບຫນ່ວຍງານທີ່ຂາດຫາຍໄປ: ຖ້າບາງສ່ວນຂອງ codebase ຂາດການທົດສອບ, ເພີ່ມພວກມັນ. ຖ້າທ່ານບໍ່ຫມັ້ນໃຈໃນການປ່ຽນແປງຂອງທ່ານ, ໃຫ້ເພີ່ມການທົດສອບເພີ່ມເຕີມ. ພວກມັນມີປະໂຫຍດເພື່ອໃຫ້ແນ່ໃຈວ່າການປ່ຽນແປງຂອງເຈົ້າບໍ່ທໍາລາຍການເຮັດວຽກທີ່ມີຢູ່.
ຖ້າແພັກເກັດຂອງເຈົ້າຂຶ້ນກັບການເອີ້ນແພັກເກັດລະດັບສູງກວ່າ: ພິຈາລະນາປ່ຽນຟັງຊັນທີ່ກ່ຽວຂ້ອງເປັນ DEPRECATED ແລ້ວເພີ່ມຟັງຊັນໃໝ່ດ້ວຍປະເພດ Error
ໃໝ່.
ສົມມຸດວ່າທ່ານກໍາລັງເຄື່ອນຍ້າຍຊຸດຖານຂໍ້ມູນ, ເຊິ່ງມີວິທີ Transaction()
:
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) }) }
ແລະມັນຖືກນໍາໃຊ້ໃນຊຸດບໍລິການຜູ້ໃຊ້:
err = s.DB(ctx).Transaction(func(tx *database.DB) error { user, usrErr := s.repo.CreateUser(ctx, tx, user) if usrErr != nil { return usrErr } }
ນັບຕັ້ງແຕ່ທ່ານກໍາລັງເຄື່ອນຍ້າຍຊຸດ database
ທໍາອິດ, ເຮັດໃຫ້ user
ແລະຫລາຍສິບແພັກເກັດອື່ນໆເປັນມັນ. ການໂທ s.repo.CreateUser()
ຍັງຄົງສົ່ງຄືນປະເພດ error
ເກົ່າ ໃນຂະນະທີ່ວິທີ Transaction()
ຕ້ອງການສົ່ງຄືນປະເພດ Error
ໃໝ່. ທ່ານສາມາດປ່ຽນວິທີ Transaction()
ເປັນ DEPRECATED
ແລະເພີ່ມວິທີການ 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) }
ເພີ່ມລະຫັດຂໍ້ຜິດພາດໃໝ່ຕາມທີ່ເຈົ້າໄປ : ເມື່ອທ່ານພົບຂໍ້ຜິດພາດທີ່ບໍ່ເໝາະສົມກັບລະຫັດທີ່ມີຢູ່ແລ້ວ, ໃຫ້ເພີ່ມລະຫັດໃໝ່. ນີ້ຈະຊ່ວຍໃຫ້ທ່ານສ້າງຊຸດລະຫັດຄວາມຜິດພາດທີ່ສົມບູນແບບໃນໄລຍະເວລາ. ລະຫັດຈາກແພັກເກັດອື່ນໆແມ່ນມີຢູ່ສະເໝີເປັນການອ້າງອີງ.
ການຈັດການຄວາມຜິດພາດໃນ Go ສາມາດຮູ້ສຶກງ່າຍດາຍໃນຕອນທໍາອິດ—ພຽງແຕ່ສົ່ງຄືນ error
ແລະສືບຕໍ່ໄປ. ແຕ່ເມື່ອ codebase ຂອງພວກເຮົາເຕີບໃຫຍ່ຂຶ້ນ, ຄວາມລຽບງ່າຍນັ້ນໄດ້ຫັນໄປສູ່ການສັບສົນຂອງບັນທຶກທີ່ບໍ່ຈະແຈ້ງ, ການຈັດການທີ່ບໍ່ສອດຄ່ອງກັນ, ແລະການແກ້ໄຂບັນຫາທີ່ບໍ່ມີທີ່ສິ້ນສຸດ.
ໂດຍການກ້າວກັບຄືນແລະຄິດຄືນໃຫມ່ວ່າພວກເຮົາຈັດການກັບຄວາມຜິດພາດແນວໃດ, ພວກເຮົາໄດ້ສ້າງລະບົບທີ່ເຮັດວຽກສໍາລັບພວກເຮົາ, ບໍ່ແມ່ນຕໍ່ຕ້ານພວກເຮົາ. ລະຫັດ namespace ສູນກາງແລະໂຄງສ້າງເຮັດໃຫ້ພວກເຮົາມີຄວາມຊັດເຈນ, ໃນຂະນະທີ່ເຄື່ອງມືສໍາລັບການສ້າງແຜນທີ່, ການຫໍ່, ແລະການທົດສອບຄວາມຜິດພາດເຮັດໃຫ້ຊີວິດຂອງພວກເຮົາງ່າຍຂຶ້ນ. ແທນທີ່ຈະລອຍຜ່ານທະເລຂອງໄມ້ທ່ອນ, ໃນປັດຈຸບັນພວກເຮົາມີຄວາມຫມາຍ, ຄວາມຜິດພາດຕິດຕາມທີ່ບອກພວກເຮົາສິ່ງທີ່ຜິດພາດແລະບ່ອນທີ່ຈະເບິ່ງ.
ກອບນີ້ບໍ່ພຽງແຕ່ກ່ຽວກັບການເຮັດໃຫ້ລະຫັດຂອງພວກເຮົາສະອາດ; ມັນກ່ຽວກັບການປະຫຍັດເວລາ, ຫຼຸດຜ່ອນຄວາມອຸກອັ່ງ, ແລະຊ່ວຍພວກເຮົາກະກຽມສໍາລັບສິ່ງທີ່ບໍ່ຮູ້. ມັນເປັນພຽງແຕ່ການເລີ່ມຕົ້ນຂອງການເດີນທາງ - ພວກເຮົາຍັງຄົ້ນພົບຮູບແບບເພີ່ມເຕີມ - ແຕ່ຜົນໄດ້ຮັບແມ່ນລະບົບທີ່ສາມາດນໍາເອົາຄວາມສະຫງົບຂອງຈິດໃຈໄປສູ່ການຈັດການຄວາມຜິດພາດ. ຫວັງເປັນຢ່າງຍິ່ງ, ມັນສາມາດເຮັດໃຫ້ເກີດຄວາມຄິດບາງຢ່າງສໍາລັບໂຄງການຂອງທ່ານເຊັ່ນກັນ! 😊
ຂ້ອຍແມ່ນ Oliver Nguyen. ຜູ້ຜະລິດຊອບແວທີ່ເຮັດວຽກສ່ວນໃຫຍ່ໃນ Go ແລະ JavaScript. ຂ້ອຍມ່ວນກັບການຮຽນຮູ້ ແລະເຫັນຕົວຕົນທີ່ດີຂຶ້ນໃນແຕ່ລະມື້. ບາງຄັ້ງການໝູນໃຊ້ໂຄງການແຫຼ່ງເປີດໃໝ່. ແບ່ງປັນຄວາມຮູ້ ແລະຄວາມຄິດໃນລະຫວ່າງການເດີນທາງຂອງຂ້ອຍ.
ໂພສຍັງຖືກເຜີຍແຜ່ຢູ່ blog.connectly.ai ແລະ olivernguyen.io 👋