Panic และ Recover
panic คืออะไร?
หัวข้อที่มีชื่อว่า “panic คืออะไร?”panic หยุดการทำงานปกติของ goroutine ปัจจุบัน goroutine หยุดรัน function ปัจจุบันทันที เริ่มคลาย call stack และรัน deferred functions ที่พบระหว่างทาง ถ้าไม่มีอะไรดักจับ panic โปรแกรมจะ crash พร้อมพิมพ์ stack trace
panic เกิดขึ้นสองแบบ:
- Runtime errors — Go runtime ทำให้เกิดอัตโนมัติสำหรับ nil pointer dereferences, การเข้าถึง slice/array นอก bounds, type assertion แบบ single-value
x.(T)บน interface value ที่ล้มเหลว และ operation ที่เป็นไปไม่ได้ที่คล้ายกัน - การเรียกโดยตรง — เรียก
panic(value)ด้วยค่าใดก็ได้ (มักเป็น string หรือ error)
panic("something that should never happen")panic(fmt.Errorf("invariant violated: %v", state))ทั้งสองรูปแบบคลาย stack เหมือนกัน ความแตกต่างเดียวคือค่าที่ recover() คืนให้
defer + recover
หัวข้อที่มีชื่อว่า “defer + recover”ทบทวนจาก Functions: deferred call ทำงานเป็น LIFO order — defer ตัวสุดท้ายที่ลงทะเบียนไว้ทำงานก่อน ระหว่าง panic ก็ยังเป็นแบบนี้: deferred function ทุกตัวบน stack ทำงานตามลำดับย้อนกลับ ก่อนที่ goroutine จะยอมแพ้ (เว้นแต่ตัวใดตัวหนึ่งเรียก 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 คืนค่า function ที่ panic จะคืนค่า ไม่ใช่กลับไปที่จุดที่เรียก panic แต่คืนค่าไปยัง caller ของ function นั้น พร้อม 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}safeCall ทำงานได้เพราะ err เป็น named return value เท่านั้น recover() รันอยู่ข้างใน deferred function ซึ่งเป็น closure แยกต่างหากจาก safeCall เอง — ทางเดียวที่มันจะมีผลต่อสิ่งที่ safeCall คืนค่าคือการ assign ให้ตัวแปรที่เป็นส่วนหนึ่งของ return signature อยู่แล้ว deferred function ไม่สามารถใช้ return err ของตัวเองแบบธรรมดาได้ การ assign ให้ named result คือช่องทางเดียวที่กลับไปหา caller
flowchart TD P["panic() called"] --> D3["deferred #3 runs (LIFO)"] D3 --> D2["deferred #2 runs"] D2 --> D1["deferred #1 runs -- calls recover() directly"] D1 --> R["recover() intercepts -- unwinding stops"] R --> Return["function returns normally to its caller"]
recover ต้องถูกเรียกโดยตรงจาก deferred function
หัวข้อที่มีชื่อว่า “recover ต้องถูกเรียกโดยตรงจาก deferred function”recover() จะดักจับ panic ได้ก็ต่อเมื่อถูกเรียกโดยตรงข้างใน function ที่เป็น deferred เท่านั้น การเรียกมันห่างออกไปหนึ่งชั้น — จาก helper function ที่ deferred function เรียกอีกที — จะไม่ทำอะไรเลย: recover() คืน nil และ panic ก็ยังคลายต่อไปเหมือนไม่เคยเรียก recover() เลย
// tryRecover calls recover(), but not directly from a deferred function --// it is one level too deep, so recover() returns nil here.func tryRecover() any { return recover()}
func riskyOp() { defer func() { r := tryRecover() // BUG: recover() is not called directly by this deferred func fmt.Println("recovered:", r) }() panic("boom")}ตรวจสอบแล้วบน 1.26.5 การเรียก riskyOp() จะพิมพ์ recovered: <nil> แล้วโปรแกรมก็ยัง crash ด้วย panic: boom — fmt.Println ทำงานจริง (พิสูจน์ว่า deferred function เองทำงาน) แต่การเรียก recover() ใน tryRecover ห่างจาก panic เกินไปที่จะดักได้ panic จึงยัง propagate ต่อไป มีแค่ recover() ที่เขียนไว้ตรง ๆ ข้างใน body ของ defer func() { ... }() เท่านั้นที่ทำงานได้
panic ใน goroutine ฆ่าทั้ง process
หัวข้อที่มีชื่อว่า “panic ใน goroutine ฆ่าทั้ง process”unrecovered panic ใน goroutine ใดก็ตาม — ไม่ใช่แค่ main — จะ crash โปรแกรมทั้งหมด และไม่มี recover() ที่ไหนช่วยหยุดมันได้เลย คู่ defer/recover ใน main ไม่ได้ป้องกัน panic ใน goroutine ที่มันสร้างขึ้นมา:
func main() { defer func() { if r := recover(); r != nil { fmt.Println("main recovered:", r) // never reached } }() go func() { panic("boom in goroutine") }() time.Sleep(100 * time.Millisecond) fmt.Println("never printed")}ตรวจสอบแล้วบน 1.26.5 โค้ดนี้ crash ทั้ง process — recover() ของ main ไม่ทำงานเลย และทั้ง "main recovered" กับ "never printed" ไม่ปรากฏเลย:
panic: boom in goroutine
goroutine 19 [running]:main.main.func1() .../main.go:10 +0x2ccreated by main.main in goroutine 1 .../main.go:9 +0x24exit status 2ทุก goroutine ที่อาจ panic ได้ต้องมี defer/recover ของตัวเองที่ระดับบนสุด การ recover ไม่ข้าม goroutine boundary ไปในทิศทางไหนเลย
log.Fatal และ os.Exit ข้าม defer
หัวข้อที่มีชื่อว่า “log.Fatal และ os.Exit ข้าม defer”os.Exit (และ log.Fatal ซึ่งเรียก os.Exit ข้างใน) ยุติ process ทันที — มันไม่คลาย stack และไม่รัน deferred function ที่ค้างอยู่เลย แม้จะลงทะเบียนไว้ก่อนหน้าใน function เดียวกันก็ตาม:
func work() { defer fmt.Println("deferred cleanup ran") // BUG: never runs fmt.Println("doing work") os.Exit(1)}ตรวจสอบแล้วบน 1.26.5 โค้ดนี้พิมพ์แค่ doing work — "deferred cleanup ran" ไม่ปรากฏเลย เพราะ os.Exit ข้าม defer ไปเลยแทนที่จะทำ unwinding ตามปกติ สงวน os.Exit/log.Fatal ไว้สำหรับ main (หรือใกล้ ๆ กับมันมาก) หลังจาก cleanup ที่สำคัญได้ทำผ่าน control flow ปกติแล้วเท่านั้น — อย่าใช้ข้างใน function ที่โค้ดอื่นคาดหวังว่าจะ cleanup ให้ตัวเองผ่าน defer
แนวทาง Must*
หัวข้อที่มีชื่อว่า “แนวทาง Must*”function ที่ชื่อ MustX (หรือ mustX ถ้าเป็น unexported) คือข้อยกเว้นที่ตั้งใจของ “คืน error อย่า panic”: มันทำงานของ X แต่ panic แทนที่จะคืน error เมื่อล้มเหลว convention นี้บอก trade-off ไว้ในชื่อเลย ทำให้ caller เห็นตั้งแต่ call site ว่าความล้มเหลวตรงนี้กู้คืนไม่ได้
regexp.MustCompile คือตัวอย่างมาตรฐาน: pattern จาก string คงที่ตอน package init นั้นถูกต้องเสมอหรือไม่ก็โปรแกรมมี bug ดังนั้นการ panic ตอน startup จึงเป็น failure mode ที่มีประโยชน์ mustPositive ใน playground ด้านล่างก็ตามรูปแบบเดียวกันสำหรับ input ที่ caller ควบคุมทั้งหมด ใช้ Must* เฉพาะเมื่อความล้มเหลวหมายถึง “caller เขียนโปรแกรมผิด ไม่ใช่เงื่อนไข runtime” เท่านั้น — อย่าใช้กับ MustX ที่ครอบ I/O, network call หรืออะไรก็ตามที่ขึ้นกับ input จากภายนอก
เมื่อไหร่ควรใช้ 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 ที่ไม่ดี regexp.MustCompile เป็นตัวอย่างที่ดีของ boundary นี้: เหมาะกับ pattern คงที่ที่รู้ตอน package init — pattern ผิดตรงนั้นคือ programmer error และการ panic ตอน startup ก็ถูกและเจอทันที ถ้า pattern มาจาก caller หรือ input ผู้ใช้แทน ให้ใช้ regexp.Compile แล้วคืน error ตามปกติ — อย่าครอบ pattern ที่มาจาก caller ด้วย MustCompile
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))} Re-panicking, runtime.Error, และ panic(nil)
หัวข้อที่มีชื่อว่า “Re-panicking, runtime.Error, และ panic(nil)”deferred function สามารถตรวจดูสิ่งที่มัน recover มาแล้วตัดสินใจว่าไม่ใช่หน้าที่ของมัน — log มันไว้ แล้วเรียก panic(r) ซ้ำด้วยค่าเดิมเพื่อให้มันคลายต่อไปเกินจุดนี้:
defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { fmt.Println("runtime error, re-panicking:", r) panic(r) // re-panic: preserve the original value and type } }}()runtime.Error คือ interface ที่ panic ทุกตัวที่ตัว runtime เองสร้างขึ้นจะ satisfy — index out of range, nil dereference, type assertion ที่ล้มเหลว และอื่น ๆ ที่คล้ายกัน การเช็คมันช่วยแยกว่า “โปรแกรมเจอสถานะที่เป็นไปไม่ได้” กับ “มีใครตั้งใจเรียก panic(someValue)” ก่อนตัดสินใจว่าจะ re-panic, log ให้มากขึ้น หรือจัดการต่างออกไป
panic(nil) ได้รับการปฏิบัติพิเศษ: ตั้งแต่ Go 1.21 การ recover มันจะไม่ได้ nil เปล่า ๆ (ซึ่งจะแยกไม่ออกจาก “ไม่มี panic เลย”) แต่จะได้ *runtime.PanicNilError แทน:
defer func() { r := recover() fmt.Printf("%v\n%T\n", r, r)}()panic(nil)ตรวจสอบแล้วบน 1.26.5 โค้ดนี้พิมพ์ panic called with nil argument และ *runtime.PanicNilError ก่อน Go 1.21 recover() หลัง panic(nil) จะคืน nil เปล่า ๆ ทำให้บอกไม่ได้ว่า “deferred function นี้รันเพราะเกิด panic” ต่างจาก “deferred function นี้รันตามปกติ” — type *runtime.PanicNilError ปิดช่องโหว่นี้
package main
import ( "fmt" "os" "runtime")
// mightRepanic recovers a runtime.Error just long enough to log it, then// re-panics with the same value so the caller still sees the failure.func mightRepanic() { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { fmt.Println("runtime error, re-panicking:", r) panic(r) // re-panic: preserve the original value and type } fmt.Println("recovered non-runtime panic:", r) } }() var s []int _ = s[5] // triggers a runtime.Error: index out of range}
func demoRepanic() { defer func() { r := recover() fmt.Println("demoRepanic recovered:", r) if _, ok := r.(runtime.Error); ok { fmt.Println("still a runtime.Error after re-panic") } }() mightRepanic()}
func demoPanicNil() { defer func() { r := recover() fmt.Printf("panic(nil) recovers as: %v (%T)\n", r, r) }() panic(nil)}
func main() { demoRepanic() demoPanicNil()
defer fmt.Println("this defer never runs -- os.Exit skips it") fmt.Println("about to exit") os.Exit(0)} อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อโค้ดที่ AI generate ใช้ panic/recover ให้มองหาสิ่งเหล่านี้ก่อนเชื่อมัน:
recover()ถูกเรียกจาก helper function แทนที่จะเขียนไว้ตรง ๆ ข้างใน body ของdefer func() { ... }()หรือเปล่า? แค่ระดับความห่างชั้นเดียวก็เพียงพอที่จะทำให้มันคืนnilเสมอ- goroutine ที่เริ่มด้วย
go func() { ... }()มีdefer/recoverของตัวเองหรือเปล่า?recover()ในmainหรือ goroutine ต้นทางไม่ได้ป้องกันมัน - function ที่ recover ใช้ named return value หรือเปล่า? การ assign ให้
errที่ไม่มีชื่อข้างใน deferred closure ไม่มีผลอะไรต่อสิ่งที่ function คืนค่าจริง os.Exitหรือlog.Fatalปรากฏข้างใน function ที่โค้ดอื่นพึ่งพาให้ cleanup ผ่านdeferหรือเปล่า? ทั้งคู่ไม่รัน defer ที่ค้างอยู่- function
MustXถูกเรียกด้วย input ที่มาจากผู้ใช้ ไฟล์ หรือ network แทนที่จะเป็นค่าคงที่ที่รู้ตอน compile/init หรือเปล่า?
🐛 หา bug ในโค้ดนี้
func tryRecover() any { return recover()}
func riskyOp() { defer func() { r := tryRecover() fmt.Println("recovered:", r) }() panic("boom")}
func main() { riskyOp() fmt.Println("reached after riskyOp")}เฉลย
recover() ทำงานได้ก็ต่อเมื่อการเรียกเขียนไว้ตรง ๆ ข้างใน deferred function เท่านั้น — ที่นี่มันถูกเรียกห่างออกไปหนึ่งชั้น ข้างใน tryRecover ซึ่ง deferred function ของ riskyOp แค่เรียกมันอีกที ตรวจสอบแล้วบน 1.26.5: โค้ดนี้พิมพ์ recovered: <nil> แล้วโปรแกรมก็ยัง crash ด้วย panic: boom — "reached after riskyOp" ไม่พิมพ์เลย ทางแก้คือเรียกตรง ๆ: r := recover() ข้างในตัว defer func() { ... }() เอง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| 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 panic ที่เกิดใน handler ให้ทันที แล้ว log stack trace และปิด connection (ไม่ได้เขียน response 500 ให้ — connection ถูกตัดไปเลย); ถ้าต้องการปิดเงียบ ๆ โดยไม่ log ให้ panic ด้วยค่า
http.ErrAbortHandlerencoding/json internal panic ถูก recover และ convert เป็น error return ก่อน expose ออก public API