paint-brush
ປ້ອງກັນຄວາມຜິດພາດຈາກການຂະຫຍາຍຕົວດ້ວຍກອບໃຫມ່ນີ້ໂດຍ@olvrng
270 ການອ່ານ

ປ້ອງກັນຄວາມຜິດພາດຈາກການຂະຫຍາຍຕົວດ້ວຍກອບໃຫມ່ນີ້

ໂດຍ Oliver Nguyen30m2024/12/11
Read on Terminal Reader

ຍາວເກີນໄປ; ອ່ານ

ນີ້ແມ່ນເລື່ອງຂອງວິທີທີ່ພວກເຮົາເລີ່ມຕົ້ນດ້ວຍວິທີການຈັດການຄວາມຜິດພາດທີ່ງ່າຍດາຍ, ໄດ້ຮັບການອຸກອັ່ງຢ່າງລະອຽດຍ້ອນວ່າບັນຫາເພີ່ມຂຶ້ນ, ແລະໃນທີ່ສຸດກໍ່ສ້າງກອບຄວາມຜິດພາດຂອງພວກເຮົາເອງ.
featured image - ປ້ອງກັນຄວາມຜິດພາດຈາກການຂະຫຍາຍຕົວດ້ວຍກອບໃຫມ່ນີ້
Oliver Nguyen HackerNoon profile picture
0-item
1-item

ການຈັດການຄວາມຜິດພາດໃນ 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() ສໍາລັບການສ້າງຄວາມຜິດພາດທີ່ງ່າຍດາຍ.
  • Wrapping errors: Wrap ຂໍ້ຜິດພາດກັບບໍລິບົດເພີ່ມເຕີມໂດຍໃຊ້ fmt.Errorf() ແລະ %w verb.
  • ການລວມຄວາມຜິດພາດ: errors.Join() ລວມຄວາມຜິດພາດຫຼາຍອັນເຂົ້າໄປໃນອັນດຽວ.
  • ການກວດສອບແລະການຈັດການຄວາມຜິດພາດ: errors.Is() ກົງກັບຄວາມຜິດພາດທີ່ມີຄ່າສະເພາະ, errors.As() ຈັບຄູ່ຂໍ້ຜິດພາດກັບປະເພດສະເພາະ, ແລະ errors.Unwrap() ດຶງຂໍ້ມູນຄວາມຜິດພາດທີ່ຕິດພັນມາ.


ໃນທາງປະຕິບັດ, ພວກເຮົາມັກຈະເຫັນຮູບແບບເຫຼົ່ານີ້:

  • ການ​ນໍາ​ໃຊ້​ຊຸດ​ມາດ​ຕະ​ຖານ​: ການ​ກັບ​ຄືນ​ຄວາມ​ຜິດ​ພາດ​ງ່າຍ​ດາຍ​ທີ່​ມີ errors.New() ຫຼື fmt.Errorf() .
  • ການສົ່ງອອກຄ່າຄົງທີ່ ຫຼືຕົວແປ: ຕົວຢ່າງ, go-redis ແລະ gorm.io ກໍານົດຕົວແປຄວາມຜິດພາດທີ່ສາມາດໃຊ້ຄືນໄດ້.
  • ປະເພດຂໍ້ຜິດພາດແບບກຳນົດເອງ: ຫ້ອງສະໝຸດເຊັ່ນ lib/pq grpc/status.Error ສ້າງປະເພດຂໍ້ຜິດພາດສະເພາະ, ມັກຈະມີລະຫັດທີ່ກ່ຽວຂ້ອງສໍາລັບບໍລິບົດເພີ່ມເຕີມ.
  • ການໂຕ້ຕອບຂໍ້ຜິດພາດກັບການປະຕິບັດ: aws-sdk-go ໃຊ້ວິທີການທີ່ອີງໃສ່ການໂຕ້ຕອບເພື່ອກໍານົດປະເພດຂໍ້ຜິດພາດກັບການປະຕິບັດຕ່າງໆ.
  • ຫຼືຫຼາຍການໂຕ້ຕອບ: ເຊັ່ນດຽວກັນກັບ Docker's erdefs , ເຊິ່ງກໍານົດການໂຕ້ຕອບຫຼາຍເພື່ອຈັດປະເພດແລະຈັດການຄວາມຜິດພາດ.

ພວກເຮົາເລີ່ມຕົ້ນດ້ວຍວິທີການທົ່ວໄປ

ໃນຊ່ວງຕົ້ນໆ, ເຊັ່ນດຽວກັບຜູ້ພັດທະນາ 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; }

ວິທີການນີ້ຊ່ວຍໃຫ້ໂຄງສ້າງຜິດພາດ, ແຕ່ເມື່ອເວລາຜ່ານໄປ, ປະເພດຂໍ້ຜິດພາດແລະລະຫັດໄດ້ຖືກເພີ່ມໂດຍບໍ່ມີແຜນການທີ່ຊັດເຈນ, ນໍາໄປສູ່ຄວາມບໍ່ສອດຄ່ອງແລະການຊໍ້າຊ້ອນ.


ແລະບັນຫາເພີ່ມຂຶ້ນຕາມເວລາ

ຂໍ້ຜິດພາດໄດ້ຖືກປະກາດຢູ່ທົ່ວທຸກແຫ່ງ

  • ແຕ່ລະຊຸດກໍານົດຄ່າຄົງທີ່ຂອງຄວາມຜິດພາດຂອງຕົນເອງໂດຍບໍ່ມີລະບົບສູນກາງ.
  • ຄົງທີ່ແລະຂໍ້ຄວາມໄດ້ຖືກກະແຈກກະຈາຍໄປທົ່ວ codebase, ເຮັດໃຫ້ມັນບໍ່ຊັດເຈນວ່າຄວາມຜິດພາດທີ່ຫນ້າທີ່ອາດຈະກັບຄືນມາ - ugh, ມັນແມ່ນ gorm.ErrRecordNotFound ຫຼື user.ErrNotFound ຫຼືທັງສອງ?


ການຫໍ່ຄວາມຜິດພາດແບບສຸ່ມເຮັດໃຫ້ບັນທຶກທີ່ບໍ່ສອດຄ່ອງກັນ ແລະ arbitrary

  • ຟັງຊັນຫຼາຍຫໍ່ຂໍ້ຜິດພາດກັບຂໍ້ຄວາມທີ່ມັກ, ບໍ່ສອດຄ່ອງໂດຍບໍ່ມີການປະກາດປະເພດຂໍ້ຜິດພາດຂອງຕົນເອງ.
  • ບັນທຶກແມ່ນ verbose, ຊໍ້າຊ້ອນ, ແລະຍາກທີ່ຈະຊອກຫາ ຫຼືຕິດຕາມ.
  • ຂໍ້ຄວາມຜິດພາດແມ່ນທົ່ວໄປແລະມັກຈະບໍ່ໄດ້ອະທິບາຍສິ່ງທີ່ຜິດພາດຫຼືມັນເກີດຂຶ້ນແນວໃດ. ຍັງເສີຍໆ ແລະມີຄວາມສ່ຽງຕໍ່ການປ່ຽນແປງທີ່ບໍ່ໄດ້ສັງເກດເຫັນ.
 unexpected gorm error: failed to find business channel: error received when invoking API: unexpected: context canceled


ບໍ່ມີມາດຕະຖານເຮັດໃຫ້ການຈັດການຄວາມຜິດພາດທີ່ບໍ່ຖືກຕ້ອງ

  • ແຕ່ລະແພັກເກັດຈັດການກັບຄວາມຜິດພາດແຕກຕ່າງກັນ, ເຮັດໃຫ້ມັນຍາກທີ່ຈະຮູ້ວ່າຫນ້າທີ່ສົ່ງຄືນ, ຫໍ່, ຫຼືການປ່ຽນຄວາມຜິດພາດ.
  • ບໍລິບົດມັກຈະສູນເສຍຍ້ອນວ່າຄວາມຜິດພາດຖືກເຜີຍແຜ່.
  • ຊັ້ນເທິງໄດ້ຮັບ ຂໍ້ຜິດພາດພາຍໃນ 500 ເຊີບເວີ ທີ່ບໍ່ຊັດເຈນໂດຍບໍ່ມີສາເຫດທີ່ຊັດເຈນ.


ບໍ່ມີການຈັດປະເພດເຮັດໃຫ້ການຕິດຕາມເປັນໄປບໍ່ໄດ້

  • ຄວາມຜິດພາດບໍ່ໄດ້ຖືກຈັດແບ່ງຕາມຄວາມຮຸນແຮງ ຫຼືພຶດຕິກໍາ: context.Canceled ຄວາມຜິດພາດທີ່ຖືກຍົກເລີກອາດຈະເປັນພຶດຕິກໍາປົກກະຕິເມື່ອຜູ້ໃຊ້ປິດແຖບບຣາວເຊີ, ແຕ່ມັນສໍາຄັນຖ້າຄໍາຮ້ອງຂໍຖືກຍົກເລີກເນື່ອງຈາກການສອບຖາມນັ້ນຊ້າແບບສຸ່ມ.
  • ບັນຫາທີ່ສໍາຄັນໄດ້ຖືກຝັງຢູ່ພາຍໃຕ້ໄມ້ທ່ອນທີ່ບໍ່ມີສຽງ, ເຮັດໃຫ້ພວກເຂົາຍາກທີ່ຈະລະບຸ.
  • ໂດຍບໍ່ມີການຈັດປະເພດ, ມັນເປັນໄປບໍ່ໄດ້ທີ່ຈະຕິດຕາມຄວາມຖີ່ຄວາມຜິດພາດ, ຄວາມຮຸນແຮງ, ຫຼືຜົນກະທົບຢ່າງມີປະສິດທິພາບ.

ມັນເຖິງເວລາທີ່ຈະສູນກາງການຈັດການຄວາມຜິດພາດ

ກັບໄປທີ່ກະດານແຕ້ມຮູບ

ເພື່ອແກ້ໄຂສິ່ງທ້າທາຍທີ່ເພີ່ມຂຶ້ນ, ພວກເຮົາໄດ້ຕັດສິນໃຈສ້າງຍຸດທະສາດຄວາມຜິດພາດທີ່ດີຂຶ້ນປະມານແນວຄວາມຄິດຫຼັກຂອງ ລະຫັດຄວາມຜິດພາດທີ່ລວມສູນແລະໂຄງສ້າງ .

  • ຄວາມຜິດພາດຖືກປະກາດຢູ່ທົ່ວທຸກແຫ່ງ → ຈັດຕັ້ງການປະກາດຄວາມຜິດພາດຢູ່ບ່ອນດຽວເພື່ອການຈັດຕັ້ງ ແລະການຕິດຕາມທີ່ດີຂຶ້ນ.
  • ບັນທຶກທີ່ບໍ່ສອດຄ່ອງ ແລະ arbitrary → ລະຫັດຄວາມຜິດພາດທີ່ມີໂຄງສ້າງທີ່ມີຮູບແບບທີ່ຊັດເຈນແລະສອດຄ່ອງ.
  • ການຈັດການຄວາມຜິດພາດທີ່ບໍ່ເຫມາະສົມ → ມາດຕະຖານການສ້າງຄວາມຜິດພາດແລະການກວດສອບປະເພດ Error ໃຫມ່ ທີ່ມີຊຸດຂອງຕົວຊ່ວຍທີ່ສົມບູນແບບ.
  • ບໍ່ມີການຈັດປະເພດ → ການຈັດປະເພດລະຫັດຂໍ້ຜິດພາດທີ່ມີແທັກສໍາລັບການກວດສອບປະສິດທິພາບຜ່ານບັນທຶກແລະການວັດແທກ.

ການຕັດສິນໃຈອອກແບບ

ລະຫັດຂໍ້ຜິດພາດທັງຫມົດແມ່ນຖືກກໍານົດຢູ່ໃນຈຸດສູນກາງທີ່ມີໂຄງສ້າງ namespace.

ໃຊ້ namespaces ເພື່ອສ້າງລະຫັດຄວາມຜິດພາດທີ່ຊັດເຈນ, ມີຄວາມຫມາຍ, ແລະຂະຫຍາຍໄດ້. ຕົວຢ່າງ:

  • PRFL.USR.NOT_FOUND ສໍາລັບ "ບໍ່ພົບຜູ້ໃຊ້."
  • FLD.NOT_FOUND ສໍາລັບ "ບໍ່ພົບເອກະສານ Flow."
  • ທັງສອງສາມາດແບ່ງປັນລະຫັດພື້ນຖານ DEPS.PG.NOT_FOUND , ຊຶ່ງຫມາຍຄວາມວ່າ "ບໍ່ພົບບັນທຶກໃນ PostgreSQL."


ແຕ່ລະຊັ້ນການບໍລິການ ຫຼືຫ້ອງສະໝຸດຈະຕ້ອງສົ່ງຄືນລະຫັດ namespace ຂອງຕົນເອງເທົ່ານັ້ນ .

  • ແຕ່ລະຊັ້ນຂອງການບໍລິການ, ຄັງເກັບມ້ຽນ, ຫຼືຫ້ອງສະຫມຸດປະກາດຊຸດລະຫັດຂໍ້ຜິດພາດຂອງຕົນເອງ.
  • ເມື່ອຊັ້ນຂໍ້ມູນໄດ້ຮັບຄວາມຜິດພາດຈາກການຂຶ້ນກັບ, ມັນຕ້ອງຫໍ່ມັນດ້ວຍລະຫັດ 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.

  • ວິສະວະກອນກໍານົດແຜນທີ່ຢູ່ໃນສະຖານທີ່ສູນກາງ, ແລະກອບຈະສ້າງລະຫັດຂໍ້ຜິດພາດຂອງແຕ່ລະລະຫັດກັບ Protobuf ErrorCode , ErrorType , gRPC status, HTTP status, ແລະ tags ສໍາລັບການບັນທຶກ / metrics.
  • ນີ້ຮັບປະກັນຄວາມສອດຄ່ອງແລະຫຼຸດຜ່ອນການຊໍ້າຊ້ອນ.

ກອບການຜິດພາດ namespace

ຊຸດຫຼັກ ແລະປະເພດ

ມີຊຸດຫຼັກຈຳນວນໜຶ່ງທີ່ເປັນພື້ນຖານຂອງກອບການຈັດການກັບຄວາມຜິດພາດອັນໃໝ່ຂອງພວກເຮົາ.

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

ການທົດສອບດ້ວຍ namespace 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 👋

L O A D I N G
. . . comments & more!

About Author

Oliver Nguyen HackerNoon profile picture
Oliver Nguyen@olvrng
I’m a software maker working mostly in Go and JavaScript. Share knowledge and thoughts during my journey.

ວາງປ້າຍ

ບົດ​ຄວາມ​ນີ້​ໄດ້​ຖືກ​ນໍາ​ສະ​ເຫນີ​ໃນ...