Day 8: Panic, Recover & Defer Semantics
Quiz: 0/16 | Score: 0 pts

Day 8: Panic, Recover Boundaries & Defer Semantics

Mastering LIFO Defer Chains, Stack Unwinding Mechanics, Panic Containment Boundaries, and Named Return Manipulation

Section 0: The "Why" & Core Vocabulary

ในสถาปัตยกรรม JVM / Kotlin / Spring Boot เรามักจะเห็นการใช้ try-catch-finally หรือ AutoCloseable / try-with-resources เพื่อดักจับข้อผิดพลาดและคืน Resource (เช่น Database Connection, Kafka Producer Lease) ซึ่งเบื้องหลังตัว Compiler ของ Java จะสร้าง Bytecode Table ขนาดใหญ่ชื่อ Exception table ที่มี Range ของ PC (Program Counter) มาควบคุมการกระโดด

The Spring Boot Anti-Pattern: Using Exceptions as Control Flow

ใน Spring Boot วิศวกรจำนวนมากคุ้นเคยกับการโยน Runtime Exception ข้าม Controller ไปยัง @ControllerAdvice เพื่อตอบ JSON 400 หรือ 500 แต่ใน Go **ห้ามทำเด็ดขาด** หากฟังก์ชันทำงานล้มเหลวอันเนื่องมาจาก Business Rules (เช่น บัญชีเงินไม่พอ, ยอดตัดเงินซ้ำ) ต้องใช้ Explicit Error Return Value เท่านั้น

Panic ในภาษา Go ออกแบบมาสำหรับ "Unrecoverable Programmer Bugs" หรือ "Violated Invariants" เท่านั้น เช่น Null Pointer Dereference, Array Index Out-of-Bounds, หรือ Config สำคัญของระบบขาดหายตอน Bootstrapping

คำศัพท์ทางเทคนิค นิยามและพฤติกรรมระดับ Runtime ใน Go เทียบเท่าในโลก Kotlin / Java
defer คำสั่งลงทะเบียน Function Call ให้ทำงาน ตอนฟังก์ชันปัจจุบันกำลังจะ Return (ก่อนที่ Call Frame จะหลุดจาก Stack) จัดเก็บเป็น LIFO Linked List บล็อก finally { ... } หรือ use { ... } / try-with-resources
panic() Built-in function ที่หยุดการทำงานปกติของ Goroutine ทันที แล้วเข้าสู่กระบวนการ Stack Unwinding ไต่ Stack Frame ขึ้นไปทีละชั้นพร้อมเรียก defer ที่ค้างอยู่ throw new Error(...) หรือ Crash unhandled exception
recover() Built-in function ที่ใช้หยุด Stack Unwinding และกู้คืน Goroutine ให้กลับมาทำงานต่อได้ ต้องเรียกตรงๆ ภายใน Deferred Function เท่านั้น บล็อก catch (Throwable t) { ... } ที่ Boundary ชั้นนอกสุด
Stack Unwinding การที่ Go Runtime ทำการไล่ Pop Stack Frame ของฟังก์ชันย้อนกลับขึ้นไปหา Caller พร้อมเคลียร์ Defer Chain จนกว่าจะเจอ recover() หรือจบ Goroutine แล้วโปรเซสแครช JVM Unwinding Call Stack เพื่อจับ Catch Block
Failure Domain เขตแดนของระบบที่ไม่ควรปล่อยให้ข้อผิดพลาดของงานหนึ่ง ลุกลามไปฆ่างานอื่น เช่น Worker หนึ่งใน Pool แครช ต้องไม่ดึงทั้งเซิร์ฟเวอร์พัง Supervisor Job / CoroutineExceptionHandler ใน Kotlin Coroutines

Section 1: Go Syntax & Command Blueprint

มาดูไวยากรณ์พื้นฐานและคำสั่งสำคัญในการควบคุม `defer`, `panic`, และ `recover` ตั้งแต่ระดับโครงสร้างโค้ด

1.1 ไวยากรณ์ `defer` และลำดับการทำงาน LIFO (Last-In-First-Out)

เมื่อใช้คีย์เวิร์ด defer ตามด้วย Function Call ตัว Runtime จะจับฟังก์ชันนั้นใส่ไว้ใน Stack ของ Defer Chain ดังนั้นคำสั่ง `defer` ที่ถูกเรียกทีหลังสุด จะได้ทำงานเป็นตัวแรกสุดเมื่อฟังก์ชัน Return:

package main

import "fmt"

func ProcessBatch() {
    defer fmt.Println("Step 1: Release DB Connection") // ทำงานลำดับที่ 3
    defer fmt.Println("Step 2: Flush Kafka Buffer")     // ทำงานลำดับที่ 2
    defer fmt.Println("Step 3: Close File Descriptor")  // ทำงานลำดับที่ 1 (LIFO)

    fmt.Println("Executing Batch Processing...")
}

1.2 กับดัก Immediate Argument Evaluation (ค่าตัวแปรถูกคำนวณทันที!)

นี่คือจุดที่โปรแกรมเมอร์ Java/Kotlin พลาดบ่อยที่สุด: Arguments ที่ส่งเข้าไปใน `defer` จะถูกประเมินผล (Evaluate) ทันที ณ บรรทัดที่เจอคำสั่ง `defer` ไม่ใช่ตอนฟังก์ชัน Return!

func TraceLatencyTrap() {
    start := time.Now()
    // จุดผิด: start ถูกอ่านค่าทันที และ time.Since(start) ถูก execute ทันทีตรงบรรทัดนี้!
    defer fmt.Println("Latency:", time.Since(start)) 

    time.Sleep(100 * time.Millisecond)
} // แสดงผล Latency: 0s (หรือระดับนาโนวินาที)

// วิธีเขียนที่ถูกต้อง: ครอบด้วย Closure (Anonymous Function)
func TraceLatencyCorrect() {
    start := time.Now()
    // Body ภายใน Closure จะยังไม่ถูก execute จนกว่าฟังก์ชันจะ Return
    defer func() {
        fmt.Println("Correct Latency:", time.Since(start))
    }()

    time.Sleep(100 * time.Millisecond)
}

1.3 การแก้ไข Named Return Values ภายใน `defer`

ใน Go ฟังก์ชันสามารถตั้งชื่อ Return Value ได้ (Named Return Values) และโค้ดใน defer สามารถเข้าถึงและแก้ค่าตัวแปรนั้นก่อนส่งคืน Caller ได้จริง:

// err ถูกตั้งชื่อไว้ใน Signature (err error)
func SafeLedgerDebit(accountID string, amount int64) (err error) {
    defer func() {
        // หากมี panic เกิดขึ้น เราจับและเปลี่ยนเป็น error ส่งกลับแทน
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from internal failure: %v", r)
        }
    }()

    if amount <= 0 {
        return fmt.Errorf("invalid debit amount: %d", amount)
    }

    // จำลองเหตุการณ์ Memory Corruption หรือ Logic Invariant พัง
    if accountID == "BAD_PAYLOAD" {
        panic("nil pointer dereference inside core ledger engine")
    }

    return nil
}

1.4 การวาง Failure Domain ด้วย Panic / Recover

ในงาน High-Throughput เช่น RFID Race Timing Ingestion หากชิปนักวิ่งส่ง Payload พิเศษที่ทำให้ Parser พัง เราต้องไม่ปล่อยให้ Goroutine นั้นพาให้ Process ของระบบ Timing ทั้งสนามแข่งตาย:

func WorkerIngestionLoop(rawEvents <-chan []byte) {
    for packet := range rawEvents {
        // วาง Boundary ภายในรอบ Loop หรือเรียกผ่านฟังก์ชันแยก
        processSinglePacketSafely(packet)
    }
}

func processSinglePacketSafely(packet []byte) {
    // 1. ลงทะเบียน recover ใน defer
    defer func() {
        if r := recover(); r != nil {
            // Log ข้อผิดพลาด และยอมให้ Loop ของ Worker ไปทำงานตัวถัดไปได้
            fmt.Printf("[CRITICAL RECOVERY] Packet ingestion panicked: %v\n", r)
        }
    }()

    // 2. Logic ประมวลผลที่อาจเกิด Panic ได้
    parseNanosecondTag(packet)
}

func parseNanosecondTag(b []byte) {
    if len(b) == 0 {
        panic("empty raw byte buffer encountered") // Panic!
    }
}

Section 2: Memory Architecture & Full Struct Definitions

ใน Go Runtime ตัว `defer` และ `panic` ไม่ใช่เรื่องเวทมนตร์ แต่ถูกจัดการผ่าน Linked List ของ Struct ที่ผูกอยู่กับ Goroutine Struct (runtime.g) โดยตรง

2.1 โครงสร้าง `_defer` ใน Go Runtime

เมื่อไหร่ก็ตามที่ Go เรียกคำสั่ง defer ตัวคอมไพเลอร์จะสร้างหรือดึง Struct _defer จาก Memory Pool มาต่อเข้ากับฟิลด์ _defer ใน runtime.g:

// src/runtime/runtime2.go (ตัดฟิลด์สำคัญเพื่อความกระจ่างเชิงสถาปัตยกรรม)
type _defer struct {
    started bool        // บอกว่า defer ตัวนี้เริ่มรันไปแล้วหรือยัง
    heap    bool        // ตัว defer นี้ถูก allocate บน Heap หรือไม่ (Open-coded vs Heap)
    openDefer bool      // โค้ดถูก Optimize ด้วย Open-coded defers ใน Go 1.14+ หรือไม่
    sp      uintptr     // Stack Pointer ของฟังก์ชันที่เป็นเจ้าของ defer
    pc      uintptr     // Program Counter ที่จะกระโดดกลับไป
    fn      func()      // Function pointer หรือ closure ที่ต้องรัน
    _panic  *_panic     // ชี้ไปยัง Panic ที่กำลัง unwind ผ่าน defer ตัวนี้ (ถ้ามี)
    link    *_defer     // ชี้ไปยัง _defer ตัวถัดไป (LIFO Linked List บน Goroutine)
    fd      unsafe.Pointer // ข้อมูลฟังก์ชันเพิ่มเติมสำหรับ Open-coded defer
    varp    uintptr     // Variable pointer
    framepc uintptr
}

2.2 โครงสร้าง `_panic` ใน Go Runtime

เมื่อเกิด panic() Runtime จะสร้าง Struct _panic เข้ามาซ้อนเป็น Linked List เช่นกัน:

// src/runtime/runtime2.go
type _panic struct {
    argp      unsafe.Pointer // ชี้ไปยัง Arguments ของ Deferred Call
    arg       any            // ค่าที่ส่งเข้ามาใน panic(val) เช่น String หรือ Error
    link      *_panic        // ชี้ไปยัง panic ก่อนหน้า (กรณีเกิด Panic ซ้อน Panic!)
    pc        uintptr        // Program Counter
    sp        unsafe.Pointer // Stack Pointer
    recovered bool           // แฟล็กแสดงสถานะว่าถูก recover() ไปแล้วหรือยัง!
    aborted   bool           // แสดงสถานะว่า panic นี้ถูกขัดจังหวะหรือไม่
    goexit    bool           // เกิดจาก runtime.Goexit หรือไม่
}

2.3 ผ่าโครงสร้าง Goroutine Header (`runtime.g`)

+-------------------------------------------------------------------------+ | Goroutine (runtime.g) | +-------------------------------------------------------------------------+ | stack: [lo, hi] - ขอบเขต Memory Stack ของ Goroutine (เริ่ม 2KB)| | _panic: *_panic - ชี้ไปยังหัวขบวน Linked List ของ Panic ที่เกิดขึ้น | | _defer: *_defer - ชี้ไปยังหัวขบวน Linked List ของ Defer (LIFO) | +------------+---------------------------------------------+--------------+ | | v v +---------------+ +---------------+ | _panic (Head) | | _defer (Top) | (LIFO: ตัวล่าสุด) | - arg: "Boom" | | - fn: Close() | | - recovered:F | | - link: ------> +---------------+ | - link: nil | +---------------+ | _defer (Next) | +---------------+ | - fn: Unlock()| | - link: nil | +---------------+
วิวัฒนาการระดับ Bare-Metal: Open-Coded Defers (Go 1.14+)

ในสมัย Go 1.12 ลงไป ทุกครั้งที่เรียก `defer` จะเกิด Heap Allocation เล็กๆ หรือต้องผ่าน Runtime Call เสมอ แต่ตั้งแต่ Go 1.14 เป็นต้นมา Go ใช้เทคนิค Open-Coded Defers โดยคอมไพเลอร์จะ Inline โค้ดของ defer เข้าไปตอนท้ายของ Function Assembly โดยตรง และใช้ตัวแปรประเภท Bitmask (1 Byte) มาคอยตรวจว่า defer จุดไหนถูกเปิดใช้งานบ้าง ทำให้ต้นทุนของ `defer` แทบจะเป็น Zero-Cost (ลดเหลือ ~1-2 นาโนวินาที) ในกรณีที่ฟังก์ชันไม่มี Loop!

Section 3: Visual Workflow & Stack Unwinding Mechanics

เมื่อเกิด Panic ขึ้น ลำดับเหตุการณ์ระดับ OS Stack Frame และ Go Runtime จะเดินทางอย่างไร? แผนผังด้านล่างแสดงขั้นตอนการ Unwinding ข้ามฟังก์ชันจนไปถึงจุดกั้นเขตแดน (Boundary):

Call Hierarchy: IngestionGateway() -> ProcessTransaction() -> ParsePayload() [1. ParsePayload() PANICS!] | |-- 1.1 หยุดการทำงานปกติทันที |-- 1.2 สร้าง struct _panic ผูกกับ Goroutine |-- 1.3 ดึง _defer ของ ParsePayload() มา execute ทั้งหมด (LIFO) |-- 1.4 ไม่มี recover() -> Pop Frame ของ ParsePayload() ทิ้ง! v [2. Stack Unwinds ขึ้นมาที่ ProcessTransaction()] | |-- 2.1 ดึง _defer ของ ProcessTransaction() มา execute |-- 2.2 ไม่มี recover() -> Pop Frame ทิ้ง! v [3. Stack Unwinds ขึ้นมาที่ IngestionGateway()] | |-- 3.1 พบ Deferred Function ที่มี recover()! |-- 3.2 recover() ตรวจสอบ _panic ปัจจุบัน -> เซ็ต recovered = true |-- 3.3 ดึงค่า panic.arg ออกมาส่งต่อให้ Application |-- 3.4 หยุดกระบวนการ Unwind! v [4. Execution Resumes Normally] IngestionGateway() ทำงานส่วนที่เหลือหลังจุด Defer หรือ Return ค่าได้ตามปกติ (Goroutine ไม่ตาย! ทำงานต่อได้)

3.1 กฎเหล็กของ `recover()`

Section 4: Edge Cases, Hazards & Idiomatic Patterns

Hazard #1: Defer Inside Loops (Memory & File Descriptor Leak)

ใน Java คุณใช้ `try-with-resources` ภายใน Loop ได้สบาย แต่ใน Go `defer` จะไม่ทำงานจนกว่าฟังก์ชันที่เป็นเจ้าของจะ Return! ไม่ใช่หมดรอบลูป!

// บั๊กอันตราย: File Descriptor หมดระบบ (Too Many Open Files)
func IngestAllLogFiles(filePaths []string) error {
    for _, path := range filePaths {
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        // ผิดมหันต์! f.Close() จะไม่ถูกเรียกในแต่ละรอบของ Loop
        // แต่จะสะสมใน Defer List จนกว่า IngestAllLogFiles จะจบฟังก์ชัน!
        defer f.Close() 
    }
    return nil
}

// วิธีแก้ระดับวิศวกร: แตก Function Boundary เพื่อให้ defer ทำงานทุกรอบลูป
func IngestAllLogFilesFixed(filePaths []string) error {
    for _, path := range filePaths {
        if err := processSingleFile(path); err != nil {
            return err
        }
    }
    return nil
}

func processSingleFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // ปลอดภัย 100% คืน Resource ทันทีที่จบฟังก์ชันย่อยนี้
    return parseData(f)
}
Hazard #2: Goroutine Panic Leak (The Whole Server Crash)

ใน Spring Boot หาก Task ใน Thread Pool ระเบิด ตัว Framework มักจะมี Catch-all ดักไว้ แต่ใน Go หากคุณยิง go func() { panic("boom") }() ออกไปโดยไม่มี `recover()` อยู่ข้างใน Goroutine นั้น ระบบจะสั่ง Crash โปรแกรมทั้ง Binary ทิ้งทันที (Exit Code 2) แม้กระทั่ง Main Goroutine ก็โดนดึงตายไปด้วย!

// Pattern มาตรฐานสำหรับการ Spawning Goroutine ปลอดภัยในระดับธนาคาร
func SafeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                // Log Stack Trace ด้วย runtime/debug
                log.Printf("[PANIC RECOVERED] Goroutine died: %v\nStack:\n%s", r, string(debug.Stack()))
            }
        }()
        fn()
    }()
}

Section 5: Knowledge Verification Quiz (16 Challenges)

ทดสอบความเข้าใจเชิงสถาปัตยกรรมระดับ Bare-Metal ทั้ง 16 ข้อ

Section 6: Hands-on Engineering Labs

Lab 1: Safe Resource Management & Closure Timing

โจทย์: เขียนฟังก์ชัน AuditExecutionTime(taskName string) (stopFn func()) โดยเมื่อเรียก defer AuditExecutionTime("LedgerSettlement")() จะต้องคำนวณเวลาการทำงานได้อย่างถูกต้อง โดยใช้ Closure หลีกเลี่ยงกับดัก Argument Immediate Evaluation

Lab 2: Failure Domain Boundary for Race Ingestion

โจทย์: เขียนฟังก์ชัน ProcessRFIDFrame(raw []byte) (err error) ใช้ Named Return Value err ร่วมกับ defer และ recover() เพื่อดักจับ Panic ภายใน แล้วแปลง Panic นั้นให้เป็น Error ปกติคืนกลับไป โดยไม่ทำให้โปรแกรมแครช

Lab 3: The Safe Worker Pool Guard

โจทย์: สร้าง Wrapper function ชื่อ RunSafeWorker(workerID int, job func()) สั่งรัน job() ใน Goroutine ใหม่ โดยต้องมี Panic Boundary ครอบอยู่ภายใน Goroutine นั้นเสมอ หากระเบิด ให้พิมพ์ข้อความเตือนและไม่ทำให้ระบบล่ม