ข้ามไปยังเนื้อหา

Panic และ Recover

panic หยุดการทำงานปกติของ goroutine ปัจจุบัน goroutine หยุดรันฟังก์ชันปัจจุบันทันที เริ่มคลาย call stack และรัน deferred functions ที่พบระหว่างทาง ถ้าไม่มีอะไรดักจับ panic โปรแกรมจะ crash พร้อมพิมพ์ stack trace

panic เกิดขึ้นสองแบบ:

  • Runtime errors — Go runtime ทำให้เกิดอัตโนมัติสำหรับ nil pointer dereferences, การเข้าถึง slice/array นอก bounds, type assertion failures บน non-interface types และ operation ที่เป็นไปไม่ได้ที่คล้ายกัน
  • การเรียกโดยตรง — เรียก panic(value) ด้วยค่าใดก็ได้ (มักเป็น string หรือ error)
panic("something that should never happen")
panic(fmt.Errorf("invariant violated: %v", state))

ทั้งสองรูปแบบคลาย stack เหมือนกัน ความแตกต่างเดียวคือค่าที่ recover() คืนให้

recover() เป็น built-in function ที่ดักจับ panic ทำงานได้เฉพาะเมื่อเรียก ภายใน deferred function เท่านั้น การเรียก recover() นอก defer จะคืน nil เสมอ

รูปแบบ canonical:

defer func() {
if r := recover(); r != nil {
// r is the value passed to panic()
fmt.Println("recovered:", r)
}
}()

เมื่อ deferred function เรียก recover() และ goroutine กำลัง panic อยู่ recover() จะหยุด panic คืนค่า panic และให้ deferred function ทำงานต่อได้ปกติ หลังจาก deferred function คืนค่า ฟังก์ชันที่ panic จะคืนค่า ไม่ใช่กลับไปที่จุดที่เรียก panic แต่คืนค่าไปยัง caller ของฟังก์ชันนั้น พร้อม zero values สำหรับ named return variables (ยกเว้นจะกำหนดใน deferred function)

การใช้งานทั่วไปคือแปลง panic เป็น error ที่ package boundary:

func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
fn()
return nil
}

panic ไม่ใช่เครื่องมือ error handling ทั่วไป กฎ: ใช้ panic สำหรับ programmer errors และสถานะที่เป็นไปไม่ได้ เงื่อนไขที่เกิดขึ้นได้เฉพาะเมื่อ code มีข้อผิดพลาด ไม่ใช่เพราะ input ไม่ดีหรือสภาวะแวดล้อม

กรณีที่เหมาะสม:

  • Invariant ถูกละเมิด — state ที่ควรเป็นไปไม่ได้ตาม structure เกิดขึ้นแล้ว แสดงว่ามี bug ในโปรแกรม
  • Package initialization failuresinit() ไม่สามารถดำเนินต่อได้เพราะ resource ที่จำเป็นขาดหายหรือตั้งค่าผิด การ panic ที่นี่ยอมรับได้เพราะโปรแกรมไม่สามารถทำงานได้ถูกต้องอยู่แล้ว
  • Unreachable branches — ทำเครื่องหมาย default case หรือ exhaustive switch branch ที่ไม่ควรรันเด็ดขาด (เป็นทั้ง documentation และ safety net)

กรณีที่ไม่เหมาะสม:

  • ไม่พบไฟล์, network timeout, input ผู้ใช้ไม่ถูกต้อง, ไม่พบ database rows สิ่งเหล่านี้คือสภาวะ runtime ที่คาดเดาได้ซึ่ง caller ต้องจัดการ คืน (T, error) สำหรับทั้งหมดนี้

library code มีข้อผูกพันเพิ่มเติม: library ไม่ควร panic กับ input ของ caller ที่ไม่ดี ถ้า operation ภายในอาจ panic (เช่น parsing ด้วย regexp.MustCompile) ให้แปลงเป็น error ที่ boundary ก่อนคืนให้ caller

สถานการณ์ใช้
Input ผู้ใช้ไม่ถูกต้องreturn error
ไม่พบไฟล์return error
Nil pointer ที่เป็นไปไม่ได้ตาม designpanic
dependency ที่จำเป็นไม่ได้ถูก initializepanic ใน init
Index out of rangepanic (runtime)
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered panic: %v", r)
}
}()
if b == 0 {
panic("division by zero")
}
return a / b, nil
}
func mustPositive(n int) int {
if n <= 0 {
panic(fmt.Sprintf("expected positive, got %d", n))
}
return n
}
func main() {
result, err := safeDivide(10, 2)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("10 / 2 =", result)
}
result, err = safeDivide(10, 0)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("10 / 0 =", result)
}
fmt.Println(mustPositive(5))
}
สิ่งที่ได้ประโยชน์ต้นทุน
panicabort ทันทีเมื่อ invariant ถูกละเมิดถ้าไม่ recover — crash program ทั้งหมด
recover ใน deferหยุด panic propagation, convert เป็น errorต้องอยู่ใน deferred function โดยตรง
panic สำหรับ programming errorชัดเจนว่า bug ไม่ใช่ expected errorยาก test, ยาก trace ใน goroutine อื่น
recover ใน server handlerป้องกัน 1 request crash ทั้ง serverอาจ hide bugs ที่ควร fix ถ้าใช้บ่อยเกินไป
  • panic = exception ที่ควรใช้สำหรับ error handling — panic ใช้สำหรับ programming error เท่านั้น (nil pointer, out of bounds, invariant violation)
  • recover ช่วยดัก panic ได้ทุกที่ — recover ทำงานเฉพาะใน deferred function ที่เรียกโดยตรงจาก panicking goroutine เท่านั้น
  • panic propagate ข้าม goroutine — panic ไม่ข้าม goroutine boundary — unrecovered panic ใน goroutine crash program ทั้งหมด
  • defer ไม่รันระหว่าง panic — defer รันเสมอรวมถึงระหว่าง panic stack unwinding — นี่คือเหตุผลที่ recover ต้องอยู่ใน defer

💡 ตัวอย่างจากของจริง

net/http server ใช้ recover ใน handler goroutine เพื่อป้องกัน 1 request panic crash ทั้ง server — panic ถูก log และ return 500 แทน

encoding/json internal panic ถูก recover และ convert เป็น error return ก่อน expose ออก public API

recover() คืนค่าอะไรถ้าเรียกนอก deferred function?
หลังจาก recover() ดักจับ panic ใน deferred function แล้ว จะเกิดอะไรขึ้น?
กรณีใดที่เหมาะสมในการใช้ panic?
deferred functions ทำงานตามลำดับใด?