ข้อกำหนดการจัดการข้อผิดพลาดใน Golang
ประเภทข้อผิดพลาด
มีตัวเลือกไม่มากในการประกาศข้อผิดพลาด ก่อนที่จะเลือกตัวเลือกที่เหมาะที่สุดสำหรับกรณีการใช้ของคุณ คำนึงถึงสิ่งต่อไปนี้:
- ผู้เรียกต้องตรงกับข้อผิดพลาดเพื่อจัดการกับมันหรือไม่? ถ้าเป็นเช่นนั้นเราต้องรองรับฟังก์ชัน
errors.Is
หรือerrors.As
โดยการประกาศตัวแปรข้อผิดพลาดระดับบนหรือประเภทที่กำหนดเอง - ข้อความข้อผิดพลาดเป็นสตริงคงที่หรือสตริงที่ต้องการข้อมูลบริบท? สำหรับสตริงคงที่เราสามารถใช้
errors.New
แต่สำหรับส่วนที่เหลือเราต้องใช้fmt.Errorf
หรือประเภทข้อผิดพลาดที่กำหนดเอง - เรากำลังส่งข้อผิดพลาดใหม่ที่ส่งกลับโดยฟังก์ชันที่อยู่ต่อไปหรือไม่? ถ้าใช่ โปรดอ้างถึงส่วนผิดพลาดการห่อ
การตรงกับข้อผิดพลาด? | ข้อความข้อผิดพลาด | แนวทาง |
---|---|---|
ไม่ | คงที่ | errors.New |
ไม่ | พลวัถ | fmt.Errorf |
ใช่ | คงที่ | ตัวแปรระดับบน var พร้อมกับ errors.New |
ใช่ | พลวัถ | ประเภท error ที่กำหนดเอง |
ตัวอย่างเช่น ใช้ errors.New
เพื่อแทนข้อผิดพลาดด้วยสตริงคงที่ ถ้าผู้เรียกต้องตรงและจัดการข้อผิดพลาดนี้ ส่งออกมันเป็นตัวแปรเพื่อรองรับการตรงกับ errors.Is
ไม่ตรงข้อผิดพลาด
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
// ไม่สามารถจัดการข้อผิดพลาด
panic("ข้อผิดพลาดที่ไม่รู้จัก")
}
ตรงข้อผิดพลาด
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// จัดการข้อผิดพลาด
} else {
panic("ข้อผิดพลาดที่ไม่รู้จัก")
}
}
สำหรับข้อผิดพลาดที่มีสตริงพลวังใช้ fmt.Errorf
ถ้าผู้เรียกไม่จำเป็นต้องตรงกับมัน ถ้าผู้เรียกจริง ๆ ต้องตรงกับมันจริง ๆ ให้ใช้อย่างอื่นที่กำหนดเอง error
ไม่ตรงข้อผิดพลาด
// package foo
func Open(file string) error {
return fmt.Errorf("ไม่พบไฟล์ %q", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// ไม่สามารถจัดการข้อผิดพลาด
panic("ข้อผิดพลาดที่ไม่รู้จัก")
}
ตรงข้อผิดพลาด
// package foo
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("ไม่พบไฟล์ %q", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// จัดการข้อผิดพลาด
} else {
panic("ข้อผิดพลาดที่ไม่รู้จัก")
}
}
โปรดทราบว่าหากคุณส่งตัวแปรหรือประเภทข้อผิดพลาดจากแพ็คเกจ มันจะเป็นส่วนหนึ่งของ API สาธารณะของแพ็คเกจนั้น
การล้อมคลุมข้อผิดพลาด
เมื่อเกิดข้อผิดพลาดในขณะเรียกใช้เมธอดอื่น ๆ มักจะมีวิธีการการจัดการทั้งสามวิธีดังนี้:
- ส่งคืนข้อผิดพลาดเดิมไว้ตามที่เป็น
- ใช้
fmt.Errorf
พร้อมกับ%w
เพื่อเพิ่มบริบทให้กับข้อผิดพลาด แล้วคืนค่าข้อผิดพลาด - ใช้
fmt.Errorf
พร้อมกับ%v
เพื่อเพิ่มบริบทให้กับข้อผิดพลาด แล้วคืนค่าข้อผิดพลาด
หากไม่มีบริบทเพิ่มเติมที่ต้องเพิ่ม ส่งคืนข้อผิดพลาดเดิมไว้ตามที่เป็น นี่เหมาะสำหรับเมื่อข้อความข้อผิดพลาดในพื้นฐานมีข้อมูลเพียงพอต่อการติดตามมาจากที่ไหนข้อผิดพลาดเกิดขึ้น
มิฉะนั้น จะเพิ่มบริบทให้กับข้อผิดพลาดให้มากที่สุดเท่าที่จะทำได้ เพื่อที่ข้อผิดพลาดที่กำกวมเช่น "การเชื่อมต่อถูกปฏิเสธ" จะไม่เกิดขึ้น จะได้ข้อผิดพลาดที่มีประโยชน์มากขึ้น เช่น "เรียกใช้บริการ foo: การเชื่อมต่อถูกปฏิเสธ"
ใช้ fmt.Errorf
เพื่อเพิ่มบริบทให้กับข้อผิดพลาดของคุณ และเลือกที่จะใช้ %w
หรือ %v
เป็นคำสิทธิ์ภายในข้อผิดพลาดโดยขึ้นอยู่กับว่าผู้เรียกควรสามารถจับคู่และดึงข้อผิดพลาดเกิดต้นหรือไม่
- ใช้
%w
หากผู้เรียกควรสามารถเข้าถึงข้อผิดพลาดที่เกิดขึ้น เราแนะนำให้ใช้ %w เป็นค่าเริ่มต้นสำหรับข้อผิดพลาดที่ล้อมครอง แต่จำได้ว่าผู้เรียกอาจเริ่มพฤติกรรมตามนี้ ดังนั้น สำหรับข้อผิดพลาดที่ล้อมครองที่เป็นตัวแปรหรือประเภทที่ทราบ บันทึกและทดสอบด้วยเป็นส่วนหนึ่งของสัญญาการทำงานของฟังก์ชัน - ใช้
%v
เพื่อทำให้ข้อผิดพลาดที่เกิดขึ้นไม่ชัดเจน ผู้เรียกจะไม่สามารถจับคู่กับมันได้ แต่คุณสามารถเปลี่ยนเป็น%w
ในอนาคตหากจำเป็น
เมื่อเพิ่มบริบทให้กับข้อผิดพลาดที่ส่งคืน หลีกเลี่ยงการใช้วลีเช่น "ล้มเหลวที่" เพื่อเก็บบริบทแบบกระชับ เมื่อข้อผิดพลาดกระจายผ่านแผง มันจะถูกซ้อนกันชั้นที่ละเอียด:
ไม่แนะนำ:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
}
// ล้มเหลวที่ x: ล้มเหลวที่ y: สร้างร้านค้าใหม่ล้มเหลว: ข้อผิดพลาด
แนะนำ:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"ร้านค้าใหม่: %w", err)
}
// x: y: ร้านค้าใหม่: ข้อผิดพลาด
อย่างไรก็ตาม เมื่อข้อผิดพลาดถูกส่งไปยังระบบอื่น ๆ ควรจะชัดเจนว่าข้อความเป็นข้อผิดพลาด (ตัวอย่างเช่น "err" หรือคำนำหน้า "ล้มเหลว" ในบันทึก)
การตั้งชื่อผิด
สำหรับค่าข้อผิดพลาดที่เก็บเป็นตัวแปรระดับ global ให้ใช้คำนำหน้า Err
หรือ err
ขึ้นอยู่กับว่ามันถูกส่งออกหรือไม่ โปรดอ้างถึงคำแนะนำ สำหรับค่าคงที่และตัวแปรระดับสูงที่ไม่ได้ส่งออกให้ใช้เส้นใต้ (_) เป็นคำนำหน้า
var (
// ส่งออกข้อผิดพลาดสองตัวด้านล่างเพื่อให้ผู้ใช้แพ็คเกจนี้สามารถจับคู่กับ errors.Is ได้
ErrBrokenLink = errors.New("ลิงก์เสีย")
ErrCouldNotOpen = errors.New("ไม่สามารถเปิด")
// ข้อผิดพลาดนี้ไม่ได้ส่งออกเพราะเราไม่ต้องการให้มันเป็นส่วนหนึ่งของ API สาธารณะ เราอาจยังสามารถใช้ได้ภายในแพ็คเกจด้วย errors
errNotFound = errors.New("ไม่พบ")
)
สำหรับประเภทข้อผิดพลาดที่กำหนดเอง ให้ใช้คำต่อท้าย Error
// ในทางเดียวกัน ข้อผิดพลาดนี้ถูกส่งออกเพื่อให้ผู้ใช้แพ็คเกจนี้สามารถจับคู่กับ errors.As
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("ไม่พบไฟล์ %q", e.File)
}
// ข้อผิดพลาดนี้ไม่ได้ส่งออกเพราะเราไม่ต้องการให้มันเป็นส่วนหนึ่งของ API สาธารณะ เรายังสามารถใช้ได้ภายในแพ็คเกจด้วย errors.As
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("แก้ไข %q", e.Path)
}
การจัดการข้อผิดพลาด
เมื่อผู้เรียกรับข้อผิดพลาดจากผู้เรียก จะสามารถจัดการกับข้อผิดพลาดในหลายวิธีตามความเข้าใจของข้อผิดพลาด
นอกจากนี้ยังรวมถึง แต่ไม่จำกัดเพียง:
- การจับคู่ข้อผิดพลาดกับ
errors.Is
หรือerrors.As
หากผู้เรียกได้ตกลงกันเกี่ยวกับความหมายของข้อผิดพลาดแน่นอนและจัดการกับการแบ่งสาขาในวิธีต่าง ๆ - การบันทึกข้อผิดพลาดและปรับลดระดับอย่างสุภาพหากข้อผิดพลาดสามารถกู้คืนได้
- การส่งข้อผิดพลาดที่นิยามไว้ดีและกลุ่มภารกิจการล้มเหลวเฉพาะด้าน
- การส่งข้อผิดพลาดไม่ว่าจะมีการคลุยร่างหรือมุมิอย่างไรก็ตาม
ไม่ว่าผู้เรียกรจะจัดการกับข้อผิดพลาดอย่างไร ควรจัดการกับข้อผิดพลาดแต่ละครั้งเท่านั้น ตัวอย่างเช่น ผู้เรียกรไม่ควรบันทึกข้อผิดพลาดแล้วส่งคืน โดยที่ผู้เรียกของมันอาจจะจัดการกับข้อผิดพลาดด้วย
ตัวอย่างเช่น พิจารณาสถานการณ์ต่อไปนี้:
ไม่ดี: บันทึกข้อผิดพลาดและส่งคืน
ผู้เรียกรบนสแต็กอื่น ๆ อาจดำเนินการเช่นเดียวกันกับข้อผิดพลาดนี้ ซึ่งจะทำให้มีเสียงรบกวนมากในบันทึกของแอปพลิเคชันโดยมีประโยชน์น้อย
u, err := getUser(id)
if err != nil {
// ไม่ดี: ดูคำอธิบาย
log.Printf("ไม่สามารถรับผู้ใช้ %q: %v", id, err)
return err
}
ดี: คลุมข้อผิดพลาดและส่งคืน
ข้อผิดพลาดบนสแต็กจะจัดการกับข้อผิดพลาดนี้ โดยใช้ %w
จะสอนให้พวกเขาสามารถจับคู่ข้อผิดพลาดกับ errors.Is
หรือ errors.As
ถ้าเกี่ยวข้อง
u, err := getUser(id)
if err != nil {
return fmt.Errorf("รับผู้ใช้ %q: %w", id, err)
}
ดี: บันทึกข้อผิดพลาดและปรับลดระดับอย่างสุภาพ
หากการดำเนินการไม่จำเป็นอย่างยิ่ง เราสามารถให้ความเอื้อเฟื้อได้อย่างสุภาพโดยไม่ต้องหยุดกระบวนการ
if err := emitMetrics(); err != nil {
// ความล้มเหลวในการเขียนเมตริกไม่ควร
// ทำให้แอปพลิเคชันขาดการทำงาน
log.Printf("ไม่สามารถส่ง metrics: %v", err)
}
ดี: จับคู่ข้อผิดพลาดและปรับลดระดับเป็นที่เหมาะสม
หากผู้เรียกได้กำหนดข้อผิดพลาดที่เฉพาะเจาะจงในข้อตกลงและการล้มเหลวสามารถกู้คืนได้ตรงนั้น จับคู่กับกรณีข้อผิดพลาดนั้นและปรับลดระดับโดยสุภาพ สำหรับกรณีอื่น ๆ คลุมข้อผิดพลาดและส่งคืน ข้อผิดพลาดบนสแต็กจะจัดการกับข้อผิดพลาดอื่น ๆ
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// ผู้ใช้ไม่มีอยู่ ใช้เวลามาตรฐานสาก
tz = time.UTC
} else {
return fmt.Errorf("รับผู้ใช้ %q: %w", id, err)
}
}
การจัดการกับการล้มเหลวในการยืนยัน
การกำหนดประเภท จะทำให้พานิกอย่างเดียวในกรณีของการตรวจจับประเภทที่ไม่ถูกต้องด้วยการคืนค่าเดียว ดังนั้นควรใช้รูปแบบ "comma, ok" เสมอ
ไม่แนะนำ:
t := i.(string)
แนะนำ:
t, ok := i.(string)
if !ok {
// จัดการกับข้อผิดพลาดอย่างสุภาพ
}
หลีกเลี่ยงการใช้ panic
การเรียกใช้โค้ดในสภาพแวดล้อมการทำงานจริงจะต้องหลีกเลี่ยงการใช้ panic โดยสิ้นเชิง เนื่องจาก panic เป็นที่มาหลักของอุปสรรคที่ต่อเนื่อง หากเกิดข้อผิดพลาด ฟังก์ชันต้องส่งคืนข้อผิดพลาดและอนุญาตให้ผู้เรียกตัดสินใจว่าจะจัดการอย่างไร
ไม่แนะนำ:
func run(args []string) {
if len(args) == 0 {
panic("ต้องการอาร์กิวเมนต์")
}
// ...
}
func main() {
run(os.Args[1:])
}
แนะนำ:
func run(args []string) error {
if len(args) == 0 {
return errors.New("ต้องการอาร์กิวเมนต์")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
panic/recover ไม่ใช่กลยุทธ์ในการจัดการข้อผิดพลาด จะต้องใช้ panic เฉพาะเมื่อเกิดเหตุการณ์ที่ไม่สามารถกู้คืนได้ (เช่น ต้านิใน) เว้นแต่ในกรณีของการเริ่มต้นโปรแกรม: สถานการณ์ที่ทำให้โปรแกรม panic จะต้องถูกจัดการในขณะเริ่มต้นโปรแกรม
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
แม้แต่ในโค้ดทดสอบ ควรใช้ t.Fatal
หรือ t.FailNow
แทนการใช้ panic เพื่อให้แน่ใจว่าความล้มเหลวได้รับการทำเครื่องหมาย
ไม่แนะนำ:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("ล้มเหลวในการกำหนดค่าทดสอบ")
}
แนะนำ:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("ล้มเหลวในการกำหนดค่าทดสอบ")
}