ข้อกำหนดการจัดการข้อผิดพลาดใน 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, &notFound) {
    // จัดการข้อผิดพลาด
  } 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("ล้มเหลวในการกำหนดค่าทดสอบ")
}