Defer, Panic และ Recover
JavaScript: try / finally / throw
หัวข้อที่มีชื่อว่า “JavaScript: try / finally / throw”TypeScript จัดการ cleanup และ error ด้วยสิ่งที่คุณคุ้นเคย: try/finally เพื่อรัน cleanup code ไม่ว่าจะเกิด error หรือไม่ และ throw/catch สำหรับการส่งต่อ error
Go มีของที่เทียบเคียงได้ครบทั้งสามตัว แต่หน้าตาต่างกันและมีพฤติกรรมที่ต่างกันในจุดสำคัญ
defer — cleanup ที่รันเสมอ
หัวข้อที่มีชื่อว่า “defer — cleanup ที่รันเสมอ”defer ตั้งเวลาเรียก function ให้รันตอนที่ function ที่ครอบอยู่ return ไม่ว่าจะ return ด้วยวิธีไหน (ปกติ, ผ่าน return, หรือหลัง panic) วิธีนี้มาแทน pattern cleanup แบบ try/finally ทั่วไป
// TypeScript — try/finally สำหรับ cleanupasync function readFile(path: string): Promise<string> { const fd = await openFile(path); try { return await fd.read(); } finally { fd.close(); // รันเสมอ }}// Go — defer แทน try/finallyfunc readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() // จะรันเมื่อ readFile return
data, err := io.ReadAll(f) if err != nil { return "", err } return string(data), nil}defer รันตาม LIFO
หัวข้อที่มีชื่อว่า “defer รันตาม LIFO”การเรียก defer หลายครั้งจะ stack ขึ้นและรัน last-in, first-out — ย้อนกลับจากลำดับที่ register ไว้ ซึ่งสะท้อนวิธีที่คุณมักต้องการ undo ขั้นตอน setup
// TypeScript — ต้องระบุลำดับใน finally อย่างชัดเจนtry { connectDB(); startTransaction(); doWork();} finally { rollbackTransaction(); // ต้องระบุเอง disconnectDB();}func doWork() { connectDB() defer disconnectDB() // register ที่ 1, รันที่ 2
startTransaction() defer rollbackTransaction() // register ที่ 2, รันที่ 1
// ถ้าขั้นตอนใด panic ทั้งสอง defer ก็ยังรัน (LIFO) processRows()}panic — เหมือน throw แต่สำหรับ error ที่กู้คืนไม่ได้
หัวข้อที่มีชื่อว่า “panic — เหมือน throw แต่สำหรับ error ที่กู้คืนไม่ได้”panic หยุด goroutine ปัจจุบัน unwind stack (รัน deferred function ทั้งหมดระหว่างทาง) แล้ว crash โปรแกรมถ้าไม่มีใครดักไว้ ใช้เฉพาะกับ programming error ที่ไม่ควรเกิดขึ้นในโค้ดที่ถูกต้อง ไม่ใช่สำหรับ error ที่คาดการณ์ได้ที่คุณต้องการส่งต่อให้ caller
// TypeScript — throw สำหรับ error ทุกประเภทfunction divide(a: number, b: number): number { if (b === 0) throw new Error("division by zero"); return a / b;}try { divide(10, 0);} catch (e) { console.error(e.message);}// Go — return error สำหรับกรณีที่คาดการณ์ได้; panic สำหรับสถานะที่เป็นไปไม่ได้func divide(a, b float64) float64 { if b == 0 { panic("หารด้วยศูนย์ — นี่คือ programming error") } return a / b}
// สำหรับ error ที่คาดการณ์ได้ ให้ return (T, error) แทน:func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("ไม่สามารถหาร %v ด้วยศูนย์", a) } return a / b, nil}recover — ดัก panic
หัวข้อที่มีชื่อว่า “recover — ดัก panic”recover หยุด panic จากการแพร่กระจาย ต้องเรียกใน deferred function เท่านั้นจึงจะทำงาน นี่คือวิธีที่ Go library (และ HTTP server) ป้องกันไม่ให้ request เดียวที่มีปัญหา crash โปรแกรมทั้งหมด
// TypeScript — catch ครอบ throwfunction safe(fn: () => void) { try { fn(); } catch (err) { console.error("caught:", err); }}// Go — recover ใน deferfunc safeRun(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }() fn() return nil}
func main() { err := safeRun(func() { panic("oh no!") }) fmt.Println(err) // panic recovered: oh no!}package main
import "fmt"
func divide(a, b float64) (result float64, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) } }() if b == 0 { panic("division by zero") } return a / b, nil}
func withCleanup() { fmt.Println("1. start") defer fmt.Println("3. cleanup (deferred)") fmt.Println("2. work")}
func multiDefer() { for i := 1; i <= 3; i++ { defer fmt.Printf("deferred %d\n", i) // LIFO: 3, 2, 1 }}
func main() { withCleanup() fmt.Println("---")
multiDefer() fmt.Println("---")
r1, err := divide(10, 2) fmt.Printf("10/2 = %.1f err=%v\n", r1, err)
r2, err := divide(5, 0) fmt.Printf("5/0 = %.1f err=%v\n", r2, err)}Loading Go runtime (first run only, ~8 MB)…