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

Defer, Panic และ Recover

TypeScript จัดการ cleanup และ error ด้วยสิ่งที่คุณคุ้นเคย: try/finally เพื่อรัน cleanup code ไม่ว่าจะเกิด error หรือไม่ และ throw/catch สำหรับการส่งต่อ error

Go มีของที่เทียบเคียงได้ครบทั้งสามตัว แต่หน้าตาต่างกันและมีพฤติกรรมที่ต่างกันในจุดสำคัญ

defer ตั้งเวลาเรียก function ให้รันตอนที่ function ที่ครอบอยู่ return ไม่ว่าจะ return ด้วยวิธีไหน (ปกติ, ผ่าน return, หรือหลัง panic) วิธีนี้มาแทน pattern cleanup แบบ try/finally ทั่วไป

TypeScript
// TypeScript — try/finally สำหรับ cleanup
async function readFile(path: string): Promise<string> {
const fd = await openFile(path);
try {
return await fd.read();
} finally {
fd.close(); // รันเสมอ
}
}
Go
// Go — defer แทน try/finally
func 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 หลายครั้งจะ stack ขึ้นและรัน last-in, first-out — ย้อนกลับจากลำดับที่ register ไว้ ซึ่งสะท้อนวิธีที่คุณมักต้องการ undo ขั้นตอน setup

TypeScript
// TypeScript — ต้องระบุลำดับใน finally อย่างชัดเจน
try {
connectDB();
startTransaction();
doWork();
} finally {
rollbackTransaction(); // ต้องระบุเอง
disconnectDB();
}
Go
func doWork() {
connectDB()
defer disconnectDB() // register ที่ 1, รันที่ 2
startTransaction()
defer rollbackTransaction() // register ที่ 2, รันที่ 1
// ถ้าขั้นตอนใด panic ทั้งสอง defer ก็ยังรัน (LIFO)
processRows()
}

panic หยุด goroutine ปัจจุบัน unwind stack (รัน deferred function ทั้งหมดระหว่างทาง) แล้ว crash โปรแกรมถ้าไม่มีใครดักไว้ ใช้เฉพาะกับ programming error ที่ไม่ควรเกิดขึ้นในโค้ดที่ถูกต้อง ไม่ใช่สำหรับ error ที่คาดการณ์ได้ที่คุณต้องการส่งต่อให้ caller

TypeScript
// 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
// 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 จากการแพร่กระจาย ต้องเรียกใน deferred function เท่านั้นจึงจะทำงาน นี่คือวิธีที่ Go library (และ HTTP server) ป้องกันไม่ให้ request เดียวที่มีปัญหา crash โปรแกรมทั้งหมด

TypeScript
// TypeScript — catch ครอบ throw
function safe(fn: () => void) {
try {
fn();
} catch (err) {
console.error("caught:", err);
}
}
Go
// Go — recover ใน defer
func 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)
}
deferred function รันเมื่อไหร่?
defer หลายครั้งใน function เดียวรันในลำดับใด?
ต้องเรียก recover() ที่ไหนเพื่อหยุด panic?
ควรใช้ panic แทนการ return error เมื่อไหร่?