Panic และ Recover
panic คืออะไร?
หัวข้อที่มีชื่อว่า “panic คืออะไร?”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() คืนให้
defer + recover
หัวข้อที่มีชื่อว่า “defer + 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
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้ panic”panic ไม่ใช่เครื่องมือ error handling ทั่วไป กฎ: ใช้ panic สำหรับ programmer errors และสถานะที่เป็นไปไม่ได้ เงื่อนไขที่เกิดขึ้นได้เฉพาะเมื่อ code มีข้อผิดพลาด ไม่ใช่เพราะ input ไม่ดีหรือสภาวะแวดล้อม
กรณีที่เหมาะสม:
- Invariant ถูกละเมิด — state ที่ควรเป็นไปไม่ได้ตาม structure เกิดขึ้นแล้ว แสดงว่ามี bug ในโปรแกรม
- Package initialization failures —
init()ไม่สามารถดำเนินต่อได้เพราะ resource ที่จำเป็นขาดหายหรือตั้งค่าผิด การ panic ที่นี่ยอมรับได้เพราะโปรแกรมไม่สามารถทำงานได้ถูกต้องอยู่แล้ว - Unreachable branches — ทำเครื่องหมาย
defaultcase หรือ 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
Panic vs error: แนวทางเร็ว
หัวข้อที่มีชื่อว่า “Panic vs error: แนวทางเร็ว”| สถานการณ์ | ใช้ |
|---|---|
| Input ผู้ใช้ไม่ถูกต้อง | return error |
| ไม่พบไฟล์ | return error |
| Nil pointer ที่เป็นไปไม่ได้ตาม design | panic |
| dependency ที่จำเป็นไม่ได้ถูก initialize | panic ใน init |
| Index out of range | panic (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))}Loading Go runtime (first run only, ~8 MB)…
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| panic | abort ทันทีเมื่อ 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