Skip to content

การจัดการข้อผิดพลาด

ใน Go มีข้อยกเว้นสามระดับ:

  • error: ข้อผิดพลาดของโฟลว์ปกติ ต้องจัดการ การเพิกเฉยโดยตรงไม่จัดการโปรแกรมก็จะไม่ล่ม
  • panic: ปัญหาร้ายแรงมาก โปรแกรมควร退出ทันทีหลังจากจัดการปัญหาแล้ว
  • fatal: ปัญหาร้ายแรงมาก โปรแกรมควร退出ทันที

อย่างถูกต้องแล้ว Go ไม่มีข้อยกเว้น มันแสดงผ่านข้อผิดพลาด ในทำนองเดียวกัน Go ก็ไม่มีคำสั่ง try-catch-finally เช่นกัน ผู้ก่อตั้ง Go หวังว่าจะควบคุมข้อผิดพลาดได้ พวกเขาไม่ต้องการให้ทุกอย่างต้องซ้อน try-catch จำนวนมาก ดังนั้นในกรณีส่วนใหญ่จะส่งคืนเป็นค่าส่งกลับของฟังก์ชัน เช่นตัวอย่างโค้ดด้านล่าง:

go
func main() {
  // เปิดไฟล์หนึ่งไฟล์
  if file, err := os.Open("README.txt"); err != nil {
    fmt.Println(err)
        return
  }
    fmt.Println(file.Name())
}

โค้ดส่วนนี้มีความหมายชัดเจนมาก เปิดไฟล์ชื่อ README.txt หากเปิดล้มเหลวฟังก์ชันจะส่งคืนข้อผิดพลาดหนึ่งข้อ พิมพ์ข้อมูลข้อผิดพลาด หากข้อผิดพลาดเป็น nil ก็แสดงว่าเปิดสำเร็จ พิมพ์ชื่อไฟล์

ดูเหมือนว่าจะกระชับกว่า try-catch เล็กน้อย แต่ถ้ามีการเรียกฟังก์ชัน特别多 จะเต็มไปด้วยคำสั่งตัดสิน if err != nil เช่นตัวอย่างด้านล่าง นี่เป็น demo การคำนวณแฮชไฟล์ ในโค้ด一小段นี้ปรากฏ if err != nil สามครั้ง

go
func main() {
  sum, err := checksum("main.go")
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(sum)
}

func checksum(path string) (string, error) {
  file, err := os.Open(path)
  if err != nil {
    return "", err
  }
  defer file.Close()

  hash := sha256.New()
  _, err = io.Copy(hash, file)
  if err != nil {
    return "", err
  }

  var hexSum [64]byte
  sum := hash.Sum(nil)
  hex.Encode(hexSum[:], sum)

  return string(hexSum[:]), nil
}

ด้วยเหตุนี้ จุดที่ถูกวิจารณ์มากที่สุดเกี่ยวกับ Go จากภายนอกคือการจัดการข้อผิดพลาด ในซอร์สโค้ด Go if err != nil ก็占ส่วน相当หนึ่ง Rust ก็ส่งคืนค่าข้อผิดพลาดเช่นกัน แต่ไม่มีใครพูดเรื่องนี้ เพราะมันแก้ปัญหานี้ผ่าน syntax sugar เมื่อเทียบกับ Rust แล้ว syntax sugar ของ Go ไม่可以说มาก ได้只能说เกือบไม่มี

แต่เราต้องมอง事物อย่างมีตรรกะ ทุกอย่างมีข้อดีและข้อเสีย ข้อดีของการจัดการข้อผิดพลาดของ Go มีหลายข้อ

  • ภาระทางจิตใจน้อย: มีข้อผิดพลาดก็จัดการ ไม่จัดการก็ส่งคืน
  • อ่านง่าย: เพราะวิธีการจัดการง่ายมาก ในกรณีส่วนใหญ่เข้าใจโค้ดได้ง่าย
  • ง่ายต่อการดีบัก: ข้อผิดพลาดทุกข้อเกิดจากค่าส่งกลับของการเรียกฟังก์ชัน สามารถย้อนกลับทีละชั้นหาได้ นานๆ ครั้งจะปรากฏข้อผิดพลาดขึ้นมาโดยไม่รู้ว่ามาจากไหน

แต่ข้อเสียก็มีไม่น้อย

  • ไม่มีข้อมูลสแต็กในข้อผิดพลาด (ต้องแก้ด้วยแพ็กเกจบุคคลที่สามหรือ封装เอง)
  • น่าเกลียด มีโค้ดซ้ำมาก (แล้วแต่ความชอบส่วนบุคคล)
  • ข้อผิดพลาดที่กำหนดเองประกาศผ่าน var เป็นตัวแปรไม่ใช่ค่าคงที่ (确实ไม่ควร)
  • ปัญหา variable shadowing

ในชุมชนมีข้อเสนอและการอภิปรายเกี่ยวกับการจัดการข้อผิดพลาดของ Go ตั้งแต่ Go เกิดมาก็ไม่เคยหยุด มีมุกตลกหนึ่งประโยค: หากคุณยอมรับการจัดการข้อผิดพลาดของ Go ได้ คุณจึงเป็น Gopher ที่ผ่านเกณฑ์

TIP

ที่นี่มีบทความสองบทความจากทีม Go เกี่ยวกับการจัดการข้อผิดพลาด สนใจสามารถดูได้

error

error เป็นข้อผิดพลาดโฟลว์ปกติชนิดหนึ่ง การปรากฏของมันยอมรับได้ ในกรณีส่วนใหญ่ควรจัดการมัน แน่นอนก็สามารถเพิกเฉยได้ ระดับความรุนแรงของ error ไม่เพียงพอที่จะหยุดโปรแกรมทั้งหมด error เองเป็นอินเทอร์เฟซที่กำหนดไว้ล่วงหน้า อินเทอร์เฟซนี้มีวิธีการเดียว Error() ค่าส่งกลับของวิธีการนี้เป็นสตริง ใช้พิมพ์ข้อมูลข้อผิดพลาด

go
type error interface {
   Error() string
}

error ในประวัติศาสตร์เคยมีการเปลี่ยนแปลงใหญ่ ในเวอร์ชัน 1.13 ทีม Go推出ข้อผิดพลาดแบบโซ่ และจัดให้มีกลไกการตรวจสอบข้อผิดพลาดที่สมบูรณ์ยิ่งขึ้น ต่อไปจะแนะนำทีละข้อ

การสร้าง

การสร้าง error มีหลายวิธี วิธีแรกคือใช้ฟังก์ชัน New ในแพ็กเกจ errors

go
err := errors.New("นี่คือข้อผิดพลาดหนึ่งข้อ")

วิธีที่สองคือใช้ฟังก์ชัน Errorf ในแพ็กเกจ fmt สามารถได้ error ที่มีพารามิเตอร์จัดรูปแบบ

go
err := fmt.Errorf("นี่คือข้อผิดพลาดที่มีพารามิเตอร์จัดรูปแบบ%dตัว", 1)

ด้านล่างนี้เป็นตัวอย่างที่สมบูรณ์

go
func sumPositive(i, j int) (int, error) {
   if i <= 0 || j <= 0 {
      return -1, errors.New("ต้องเป็นจำนวนเต็มบวก")
   }
   return i + j, nil
}

ในกรณีส่วนใหญ่ เพื่อการบำรุงรักษาที่ดีขึ้น โดยทั่วไปจะไม่สร้าง error ชั่วคราว แต่จะใช้ error ที่ใช้บ่อยเป็นตัวแปรโกลบอล เช่นโค้ดที่ตัดมาจากไฟล์ os\erros.go ด้านล่าง

go
var (
  ErrInvalid = fs.ErrInvalid // "invalid argument"

  ErrPermission = fs.ErrPermission // "permission denied"
  ErrExist      = fs.ErrExist      // "file already exists"
  ErrNotExist   = fs.ErrNotExist   // "file does not exist"
  ErrClosed     = fs.ErrClosed     // "file already closed"

  ErrNoDeadline       = errNoDeadline()       // "file type does not support deadline"
  ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)

จะเห็นว่าทั้งหมดถูกกำหนดโดย var เป็นตัวแปร

ข้อผิดพลาดที่กำหนดเอง

ผ่านการนำไปใช้วิธีการ Error() สามารถกำหนด error เองได้ง่ายๆ เช่น errorString ในแพ็กเกจ erros เป็นการนำไปใช้อย่างง่ายมาก

go
func New(text string) error {
   return &errorString{text}
}

// สตรักต์ errorString
type errorString struct {
   s string
}

func (e *errorString) Error() string {
   return e.s
}

เนื่องจาก errorString นำไปใช้เรียบง่ายเกินไป ความสามารถในการแสดงไม่เพียงพอ ดังนั้นไลบรารีโอเพนซอร์สจำนวนมาก รวมถึงไลบรารีทางการ จะเลือกกำหนด error เอง เพื่อตอบสนองความต้องการข้อผิดพลาดที่ต่างกัน

การส่งต่อ

ในบางสถานการณ์ ผู้เรียกเรียกฟังก์ชันที่ส่งคืนข้อผิดพลาดหนึ่งข้อ แต่ผู้เรียกเองไม่รับผิดชอบจัดการข้อผิดพลาด จึงส่งคืนข้อผิดพลาดเป็นค่าส่งกลับเช่นกัน โยนให้ผู้เรียกชั้นบน กระบวนการนี้เรียกว่าการส่งต่อ ข้อผิดพลาดในระหว่างกระบวนการส่งต่ออาจถูก包装หลายชั้น เมื่อผู้เรียกชั้นบนต้องการตัดสินประเภทของข้อผิดพลาดเพื่อทำการจัดการที่ต่างกัน อาจไม่สามารถแยกแยะประเภทของข้อผิดพลาดหรือตัดสินผิด และข้อผิดพลาดแบบโซ่เกิดขึ้นเพื่อแก้สถานการณ์นี้

go
type wrapError struct {
   msg string
   err error
}

func (e *wrapError) Error() string {
   return e.msg
}

func (e *wrapError) Unwrap() error {
   return e.err
}

wrappError ก็นำไปใช้อินเทอร์เฟซ error เช่นกัน มีวิธีการเพิ่มหนึ่งวิธี Unwrap ใช้ส่งคืนการอ้างอิงสำหรับ error เดิมภายใน ภายใต้การ包装หลายชั้นก็เกิดเป็นโซ่ข้อผิดพลาดหนึ่งโซ่ ตามโซ่หาไป ก็หา error เดิมได้ง่ายๆ เนื่องจากสตรักต์นี้ไม่เปิดเผยต่อภายนอก จึงสามารถใช้ฟังก์ชัน fmt.Errorf ในการสร้างได้เท่านั้น เช่น

go
err := errors.New("นี่คือข้อผิดพลาดเดิม")
wrapErr := fmt.Errorf("ข้อผิดพลาด %w", err)

เมื่อใช้ ต้องใช้คำกริยาจัดรูปแบบ %w และพารามิเตอร์ต้องเป็น error ที่มีผลหนึ่งข้อเท่านั้น

การจัดการ

ขั้นตอนสุดท้ายในการจัดการข้อผิดพลาดคือวิธีการจัดการและตรวจสอบข้อผิดพลาด แพ็กเกจ errors จัดให้มีฟังก์ชันสะดวกหลายฟังก์ชันสำหรับจัดการข้อผิดพลาด

go
func Unwrap(err error) error

ฟังก์ชัน errors.Unwrap() ใช้แกะ包装โซ่ข้อผิดพลาดหนึ่งโซ่ การนำไปใช้ภายในง่ายมาก

go
func Unwrap(err error) error {
   u, ok := err.(interface { // type assertion ว่านำไปใช้วิธีการนี้หรือไม่
      Unwrap() error
   })
   if !ok { // ไม่ได้นำไปใช้แสดงว่าเป็น error พื้นฐาน
      return nil
   }
   return u.Unwrap() // มิฉะนั้นเรียก Unwrap
}

หลังจากแกะ包装แล้วจะส่งคืนข้อผิดพลาดที่โซ่ข้อผิดพลาดปัจจุบัน包装อยู่ ข้อผิดพลาดที่ถูก包装อาจยังคงเป็นโซ่ข้อผิดพลาด หากต้องการหาค่าหรือประเภทที่สอดคล้องกันในโซ่ข้อผิดพลาด สามารถค้นหาจับคู่แบบ recursive ได้ แต่ไลบรารีมาตรฐานจัดให้มีฟังก์ชันที่คล้ายกันไว้แล้ว

go
func Is(err, target error) bool

ฟังก์ชัน errors.Is ใช้ตัดสินว่าโซ่ข้อผิดพลาดมีข้อผิดพลาดที่ระบุหรือไม่ ตัวอย่างดังนี้

go
var originalErr = errors.New("this is an error")

func wrap1() error { //包装ข้อผิดพลาดเดิม
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // ข้อผิดพลาดเดิม
   return originalErr
}

func main() {
   err := wrap1()
   if errors.Is(err, originalErr) { // หากใช้ if err == originalErr จะเป็น false
      fmt.Println("original")
   }
}

ดังนั้นเมื่อตัดสินข้อผิดพลาด ไม่ควรใช้ตัวดำเนินการ == แต่ควรใช้ errors.Is()

go
func As(err error, target any) bool

ฟังก์ชัน errors.As() ใช้ค้นหาข้อผิดพลาดประเภทที่จับคู่แรกในโซ่ข้อผิดพลาด และกำหนดค่าให้กับ err ที่ส่งเข้าไป ในบางกรณีต้องการแปลงข้อผิดพลาดประเภท error เป็นประเภทการนำไปใช้ข้อผิดพลาดที่เฉพาะเจาะจง เพื่อให้ได้รายละเอียดข้อผิดพลาดที่ละเอียดมากขึ้น และการใช้ type assertion กับโซ่ข้อผิดพลาดหนึ่งโซ่ไม่มีผล เพราะข้อผิดพลาดเดิมถูกสตรักต์包装ไว้ นี่คือเหตุผลที่ต้องการฟังก์ชัน As ตัวอย่างดังนี้

go
type TimeError struct { // กำหนด error เอง
   Msg  string
   Time time.Time //บันทึกเวลาที่เกิดข้อผิดพลาด
}

func (m TimeError) Error() string {
   return m.Msg
}

func NewMyError(msg string) error {
   return &TimeError{
      Msg:  msg,
      Time: time.Now(),
   }
}

func wrap1() error { //包装ข้อผิดพลาดเดิม
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // ข้อผิดพลาดเดิม
   return NewMyError("original error")
}

func main() {
   var myerr *TimeError
   err := wrap1()
   // ตรวจสอบว่าในโซ่ข้อผิดพลาดมีข้อผิดพลาดประเภท *TimeError หรือไม่
   if errors.As(err, &myerr) { // พิมพ์เวลาของ TimeError
      fmt.Println("original", myerr.Time)
   }
}

target ต้องเป็นพอยน์เตอร์ที่ชี้ไปยัง error เนื่องจากในการสร้างสตรักต์ส่งคืนเป็นพอยน์เตอร์สตรักต์ ดังนั้น error จริงๆ แล้วเป็นประเภท *TimeError ดังนั้น target ต้องเป็นประเภท **TimeError

แต่แพ็กเกจ errors ที่ทางการจัดให้其实ไม่เพียงพอ เพราะไม่มีข้อมูลสแต็ก ไม่สามารถ定位ได้ โดยทั่วไปจะแนะนำใช้แพ็กเกจเสริมอีกตัวหนึ่งของทางการ

github.com/pkg/errors

ตัวอย่าง

go
import (
  "fmt"
  "github.com/pkg/errors"
)

func Do() error {
  return errors.New("error")
}

func main() {
  if err := Do(); err != nil {
    fmt.Printf("%+v", err)
  }
}

ผลลัพธ์

some unexpected error happened
main.Do
        D:/WorkSpace/Code/GoLeran/golearn/main.go:9
main.main
        D:/WorkSpace/Code/GoLeran/golearn/main.go:13
runtime.main
        D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
runtime.goexit
        D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650

ผ่านการพิมพ์แบบจัดรูปแบบ ก็เห็นข้อมูลสแต็กแล้ว โดยค่าเริ่มต้นจะไม่พิมพ์สแต็ก แพ็กเกจนี้เทียบเท่ากับเวอร์ชันเสริมของแพ็กเกจ errors ในไลบรารีมาตรฐาน ทั้งสองเขียนโดยทางการเหมือนกัน ไม่รู้ว่าทำไมไม่รวมเข้าไลบรารีมาตรฐาน

panic

panic แปลว่าความตื่นตระหนก แสดงถึงปัญหาโปรแกรมที่ร้ายแรงมาก โปรแกรมต้องหยุดทันทีเพื่อจัดการปัญหานี้ มิฉะนั้นโปรแกรมหยุดทำงานทันทีและพิมพ์ข้อมูลสแต็ก panic เป็นรูปแบบการแสดงข้อยกเว้นขณะรันไทม์ของ Go มักปรากฏในการดำเนินการที่อันตรายบางประการ ส่วนใหญ่เพื่อหยุดความเสียหายทันที เพื่อหลีกเลี่ยงผลที่ร้ายแรงยิ่งขึ้น แต่ panic ก่อน退出จะทำงาน善后ของโปรแกรมให้เรียบร้อย ในเวลาเดียวกัน panic ก็สามารถถูกกู้คืนเพื่อให้โปรแกรมทำงานต่อได้

ด้านล่างนี้เป็นตัวอย่างการเขียนค่าลงใน map ที่เป็น nil แน่นอนว่าจะ触发 panic

go
func main() {
   var dic map[string]int
   dic["a"] = 'a'
}
panic: assignment to entry in nil map

TIP

เมื่อโปรแกรมมี goroutine หลายตัว ตราบใดที่ goroutine ใด goroutine หนึ่งเกิด panic หากไม่จับมัน โปรแกรมทั้งหมดจะล่ม

การสร้าง

การสร้าง panic อย่างชัดเจนง่ายมาก ใช้ฟังก์ชันในตัว panic即可 ลายเซ็นฟังก์ชันมีดังนี้

go
func panic(v any)

ฟังก์ชัน panic รับพารามิเตอร์ v ประเภท any เมื่อพิมพ์ข้อมูลสแต็กข้อผิดพลาด v ก็จะถูกพิมพ์ด้วย ตัวอย่างการใช้งานมีดังนี้

go
func main() {
  initDataBase("", 0)
}

func initDataBase(host string, port int) {
  if len(host) == 0 || port == 0 {
    panic("พารามิเตอร์การเชื่อมต่อข้อมูลไม่ถูกต้อง")
  }
    // ...ตรรกะอื่นๆ
}

เมื่อเริ่มต้นการเชื่อมต่อฐานข้อมูลล้มเหลว โปรแกรมก็ไม่ควร启动 เพราะไม่มีฐานข้อมูลโปรแกรมก็ทำงานไร้ความหมาย ดังนั้นที่นี่ควรโยน panic

panic: พารามิเตอร์การเชื่อมต่อข้อมูลไม่ถูกต้อง

งาน善后

ก่อนโปรแกรม退出เนื่องจาก panic จะทำงาน善后บางประการ เช่นดำเนินการคำสั่ง defer

go
func main() {
   defer fmt.Println("A")
   defer fmt.Println("B")
   fmt.Println("C")
   panic("panic")
   defer fmt.Println("D")
}

ผลลัพธ์เป็น

C
B
A
panic: panic

และคำสั่ง defer ของฟังก์ชัน上游ก็จะดำเนินการเช่นกัน ตัวอย่างดังนี้

go
func main() {
   defer fmt.Println("A")
   defer fmt.Println("B")
   fmt.Println("C")
   dangerOp()
   defer fmt.Println("D")
}

func dangerOp() {
   defer fmt.Println(1)
   defer fmt.Println(2)
   panic("panic")
   defer fmt.Println(3)
}

ผลลัพธ์

C
2
1
B
A
panic: panic

ใน defer ก็สามารถซ้อน panic ได้ ด้านล่างนี้เป็นตัวอย่างที่ค่อนข้างซับซ้อน

go
func main() {
  defer fmt.Println("A")
  defer func() {
    func() {
      panic("panicA")
      defer fmt.Println("E")
    }()
  }()
  fmt.Println("C")
  dangerOp()
  defer fmt.Println("D")
}

func dangerOp() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
}

ลำดับการดำเนินการของ panic ที่ซ้อนใน defer ยังคงเหมือนเดิม เมื่อเกิด panic แล้ว ตรรกะที่ตามมาจะไม่สามารถดำเนินการได้

C
2
1
A
panic: panicB
        panic: panicA

สรุปแล้ว เมื่อเกิด panic จะ退出ฟังก์ชันที่ตนอยู่ทันที และทำงาน善后ของฟังก์ชันปัจจุบัน เช่น defer แล้วโยนขึ้นชั้นบน ฟังก์ชัน上游ก็ทำงาน善后เช่นกัน จนกว่าโปรแกรมหยุดทำงาน

เมื่อ goroutine ย่อยเกิด panic จะไม่触发งาน善后ของ goroutine ปัจจุบัน หากจนกว่า goroutine ย่อย退出แล้วยังไม่กู้คืน panic แล้ว โปรแกรมจะหยุดทำงานทันที

go
var waitGroup sync.WaitGroup

func main() {
  demo()
}

func demo() {
  waitGroup.Add(1)
  defer func() {
    fmt.Println("A")
  }()
  fmt.Println("C")
  go dangerOp()
  waitGroup.Wait() // goroutine พ่อบล็อกและรอกoroutine ย่อยดำเนินการเสร็จ
  defer fmt.Println("D")
}
func dangerOp() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
  waitGroup.Done()
}

ผลลัพธ์เป็น

C
2
1
panic: panicB

จะเห็นว่าคำสั่ง defer ใน demo() หนึ่งข้อก็ไม่ดำเนินการ โปรแกรมก็退出แล้ว ควรสังเกตว่า หากไม่มี waitGroup เพื่อบล็อก goroutine พ่อ แล้ว การดำเนินการของ demo() อาจเร็วกว่าความเร็วการดำเนินการของ goroutine ย่อย ผลลัพธ์ที่พิมพ์ออกมาก็จะสับสนมาก ด้านล่างนี้แก้ไขโค้ดเล็กน้อย

go
func main() {
  demo()
}

func demo() {
  defer func() {
    // งาน善后ของ goroutine พ่อใช้เวลา 20ms
    time.Sleep(time.Millisecond * 20)
    fmt.Println("A")
  }()
  fmt.Println("C")
  go dangerOp()
  defer fmt.Println("D")
}
func dangerOp() {
  // goroutine ย่อยต้องดำเนินการบางอย่าง ใช้เวลา 1ms
  time.Sleep(time.Millisecond)
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
}

ผลลัพธ์เป็น

C
D
2
1
panic: panicB

ในตัวอย่างนี้ เมื่อ goroutine ย่อยเกิด panic แล้ว goroutine พ่อเสร็จสิ้นการดำเนินการของฟังก์ชันแล้ว เข้าสู่งาน善后 แล้ว在执行 defer สุดท้าย พอดีพบ goroutine ย่อยเกิด panic ดังนั้นโปรแกรมก็退出การทำงาน

การกู้คืน

เมื่อเกิด panic แล้ว ใช้ฟังก์ชันในตัว recover() สามารถจัดการทันเวลาและรับประกันว่าโปรแกรมทำงานต่อได้ ต้องดำเนินการในคำสั่ง defer เท่านั้น ตัวอย่างการใช้งานมีดังนี้

go
func main() {
   dangerOp()
   fmt.Println("โปรแกรม退出ปกติ")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("กู้คืน panic แล้ว")
      }
   }()
   panic("เกิด panic")
}

ผู้เรียกไม่รู้ว่าภายในฟังก์ชัน dangerOp() เกิด panic เลย โปรแกรมดำเนินการตรรกะที่เหลือแล้ว退出ปกติ ดังนั้นผลลัพธ์เป็นดังนี้

เกิด panic
กู้คืน panic แล้ว
โปรแกรม退出ปกติ

แต่จริงๆ แล้วการใช้ recover() มีกับดักโดยนัยหลายประการ เช่นการใช้ closure ใน defer ใช้ recover อีกครั้ง

go
func main() {
  dangerOp()
  fmt.Println("โปรแกรม退出ปกติ")
}

func dangerOp() {
  defer func() {
    func() {
      if err := recover(); err != nil {
        fmt.Println(err)
        fmt.Println("กู้คืน panic แล้ว")
      }
    }()
  }()
  panic("เกิด panic")
}

ฟังก์ชัน closure สามารถพิจารณาว่าเรียกฟังก์ชันหนึ่งฟังก์ชัน panic ส่งขึ้นข้างบนไม่ใช่ลงข้างล่าง แน่นอนว่าฟังก์ชัน closure ก็ไม่สามารถกู้คืน panic ได้ ดังนั้นผลลัพธ์เป็นดังนี้

panic: เกิด panic

นอกจากนั้น ยังมีสถานการณ์ที่รุนแรงมาก นั่นคือพารามิเตอร์ของ panic() เป็น nil

go
func main() {
   dangerOp()
   fmt.Println("โปรแกรม退出ปกติ")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("กู้คืน panic แล้ว")
      }
   }()
   panic(nil)
}

ในกรณีนี้ panic จริงๆ แล้วจะกู้คืน แต่จะไม่พิมพ์ข้อมูลข้อผิดพลาดใดๆ

ผลลัพธ์

โปรแกรม退出ปกติ

สรุปแล้วฟังก์ชัน recover มีข้อควรสังเกตหลายข้อ

  1. ต้องใช้ใน defer เท่านั้น
  2. ใช้หลายครั้งก็มีเพียงหนึ่งครั้งที่สามารถกู้คืน panic ได้
  3. closure recover จะไม่กู้คืน panic ใดๆ ของฟังก์ชันภายนอก
  4. พารามิเตอร์ของ panic ห้ามใช้ nil

fatal

fatal เป็นปัญหาร้ายแรงมาก เมื่อเกิด fatal แล้ว โปรแกรมต้องหยุดทำงานทันที ไม่ทำงาน善后ใดๆ โดยปกติแล้วจะเรียกฟังก์ชัน Exit ในแพ็กเกจ os เพื่อ退出โปรแกรม ดังแสดงด้านล่าง

go
func main() {
  dangerOp("")
}

func dangerOp(str string) {
  if len(str) == 0 {
    fmt.Println("fatal")
    os.Exit(1)
  }
  fmt.Println("ตรรกะปกติ")
}

ผลลัพธ์

fatal

ปัญหา级别 fatal โดยทั่วไปไม่ค่อย触发อย่างชัดเจน ส่วนใหญ่เป็น被动触发

Golang by www.golangdev.cn edit