Day 7: Error Inspection Patterns
Quiz: 0/16 | Score: 0 pts

Day 7: Error Inspection Patterns

Mastering errors.Is, errors.As, Contextual Wrapping (%w), and Bare-Metal Error Tree Traversal

Section 0: The "Why" & Core Vocabulary

ในฐานะวิศวกรจากโลก JVM / Spring Boot สิ่งที่คุณคุ้นเคยที่สุดคือ Exception Handling: เมื่อเกิดปัญหาทางเทคนิค เช่น Database Timeout หรือ Validation ผิดพลาด คุณจะทำการ throw new PaymentProcessingException(...) และปล่อยให้ Framework คอยจับผ่าน @ExceptionHandler

The JVM Trap: Cost of Stack Traces & Hidden Control Flow

ใน JVM เมื่อเรียก throw new Exception() ตัว Runtime ต้องรันเมธอด fillInStackTrace() แบบ Native System Call เพื่อเดินทางไต่ Thread Stack Frames ย้อนกลับขึ้นไป บันทึก Source Line Numbers, Method Names, และ Class References ทั้งหมด ซึ่งทำให้เกิด CPU Latency Spike อย่างรุนแรงในระดับ Microseconds (High TPS) และทำให้ Flow ของโค้ดขาดความโปร่งใส (GOTO ซ่อนรูป)

Go ตัดกลไก Exception ทิ้งเกือบทั้งหมดสำหรับ Control Flow ทั่วไป แล้วกำหนดให้ Error คือค่าธรรมดาตัวหนึ่ง (Values) ที่ส่งกลับมาทาง Explicit Multiple Return Values ตามแนวคิด: "Errors are just values. Values can be programmed." (Rob Pike)

คำศัพท์ทางเทคนิค (Go) นิยามและพฤติกรรมระดับ Runtime แนวคิดเทียบเท่าใน Kotlin / Spring
Sentinel Error ตัวแปรคงที่ (Package-level error constant/variable) นิยมตั้งชื่อขึ้นต้นด้วย Err... เก็บ Pointer หรือค่าที่ชัดเจนเพื่อใช้เปรียบเทียบความเท่ากัน (Equality check) Pre-defined Exception singleton เช่น NoSuchElementException หรือ Result Sealed Subclasses
Custom Error Type Struct ที่ทำการ Implement เมธอด Error() string เพื่อเก็บ Context เพิ่มเติม (เช่น Ledger AccountID, Balance, Timestamp) Custom Checked/Unchecked Exception class เช่น InsufficientFundsException
Error Wrapping การห่อหุ้ม Error ชั้นในไว้ใน Error ชั้นนอกด้วย fmt.Errorf("...: %w", err) โดยสร้าง Dynamic Node ซ้อนกันเป็น Linked List (Tree) เพื่อรักษาต้นตอ (Root Cause) Exception Chaining (new Exception("msg", causeException))
Unwrap() Method Interface contract ที่คืนค่า error ตัวถัดไป (หรือ []error ใน Go 1.20+) เพื่อให้ Runtime ใช้เดินทางสืบหาบรรพบุรุษ เมธอด throwable.getCause()
errors.Is ฟังก์ชันตรวจเช็กว่า Tree ของ Error มี Node ใด Node หนึ่งตรงกับ Target Error หรือไม่ (Equality & Semantic Match) e is SpecificException หรือการตรวจสอบ Exception Cause ซ้ำๆ
errors.As ฟังก์ชันค้นหา Node ใน Error Tree ที่สามารถ Type-Cast ไปเป็น Target Pointer Type ได้ พร้อมดึงข้อมูลออกมาใช้งาน catch (e: SpecificException) พร้อมเข้าถึง Field ภายใน

Section 1: Go Syntax & Command Blueprint

ใน Go ไวยากรณ์ของ error ไม่ใช่ Class พิเศษ แต่เป็น Built-in Single-Method Interface ระดับรากฐานของภาษา:

// นิยามของ error ใน Built-in Package ของ Go
type error interface {
    Error() string
}

เมื่อไหร่ก็ตามที่ Struct ใดก็ตาม implement เมธอด Error() string ค่าของ Struct ตัวนั้นจะถือเป็น error ทันทีตามกฎ Implicit Interface ของ Go (ไม่มีคีย์เวิร์ด implements เหมือน Java)

1.1 Sentinel Errors vs Simple Errors

การสร้าง Error พื้นฐานด้วย Standard Library มี 2 วิธีหลัก:

package main

import (
    "errors"
    "fmt"
)

// 1. ประกาศ Package-level Sentinel Error (ตัวแปรแบบ immutable intent)
var (
    ErrAccountFrozen     = errors.New("ledger: account is frozen")
    ErrInsufficientFunds = errors.New("ledger: insufficient funds")
)

func DeductBalance(balance, amount int64) (int64, error) {
    if amount > balance {
        // คืนค่า Sentinel Error โดยตรง (คืน 0, error)
        return balance, ErrInsufficientFunds
    }
    return balance - amount, nil // nil หมายถึงไม่มี error (คล้าย null แต่เป็น Type interface)
}

1.2 Error Wrapping ด้วย %w Verb

เมื่อคุณเรียกใช้งานฟังก์ชันระดับล่าง (เช่น Storage/DB) แล้วต้องการแนบ Context เพิ่มเติม (เช่น Account ID) ห้ามใช้ %v หรือ %s หากต้องการให้ Caller ยังคงตรวจสอบต้นตอได้ ให้ใช้ %w (Wrap):

func ProcessWireTransfer(accID string, balance, amount int64) error {
    _, err := DeductBalance(balance, amount)
    if err != nil {
        // %w จะเรียกสร้าง struct wrapError ที่ implement เมธอด Unwrap() อัตโนมัติ
        return fmt.Errorf("transfer failed for account %s: %w", accID, err)
    }
    return nil
}
ข้อแตกต่างที่สำคัญมากระหว่าง %w และ %v

- fmt.Errorf("...: %v", err) แปลง Error ให้กลายเป็น Plain String ธรรมดา สายโซ่ (Chain) ขาดทันที ฟังก์ชัน errors.Is และ errors.As จะไม่สามารถแกะหา Root Cause ได้อีกต่อไป
- fmt.Errorf("...: %w", err) รักษา Reference ของ Struct เดิมไว้ใน Pointer ภายใน Caller สามารถใช้ errors.Is หรือ errors.As สืบย้อนกลับไปได้เสมอ

1.3 การตรวจสอบด้วย errors.Is

err := ProcessWireTransfer("ACC-9021", 100, 500)
if err != nil {
    // เดิน Loop สืบหาใน Tree ว่าต้นตอคือ ErrInsufficientFunds หรือไม่
    if errors.Is(err, ErrInsufficientFunds) {
        // จัดการกรณีเงินไม่พอ (เช่น ส่งแจ้งเตือนหักเงินไม่สำเร็จ)
        fmt.Println("Handle: Alert customer to top up funds")
        return
    }
    // กรณีเป็น System Error อื่นๆ
    fmt.Printf("Fatal transaction error: %v\n", err)
}

1.4 การตรวจสอบและดึง Struct ด้วย errors.As

จุดผิดพลาดอันดับ 1 ของมือใหม่คือ Argument ของ errors.As ต้องเป็น "Pointer to Pointer" หรือ "Pointer to Interface" เสมอ มิฉะนั้น Runtime จะสั่ง panic ทันที:

// ประกาศ Custom Struct สำหรับ Error ในระบบวิ่งมาราธอน
type RFIDScanError struct {
    RunnerID  string
    AntennaID int
    RawSignal int
}

func (e *RFIDScanError) Error() string {
    return fmt.Sprintf("rfid: bad signal %ddBm on antenna %d for runner %s", e.RawSignal, e.AntennaID, e.RunnerID)
}

func IngestRFIDPacket(packet []byte) error {
    // สมมติว่าเกิดข้อผิดพลาด
    return fmt.Errorf("pipeline stage 1: %w", &RFIDScanError{
        RunnerID:  "BIB-42195",
        AntennaID: 4,
        RawSignal: -98,
    })
}

func HandlePacket() {
    err := IngestRFIDPacket(nil)
    if err != nil {
        // target ต้องเป็น Pointer ไปยัง Type ของ struct ที่ implement error
        var scanErr *RFIDScanError
        
        // ส่ง &scanErr (Pointer to Pointer) เข้าไปใน errors.As
        if errors.As(err, &scanErr) {
            // สำเร็จ! scanErr ถูก assign ค่า instance จากใน chain มาให้เรียบร้อย
            fmt.Printf("Dropped packet for runner %s on antenna %d\n", scanErr.RunnerID, scanErr.AntennaID)
            return
        }
        fmt.Println("Unknown ingestion error:", err)
    }
}

Section 2: Memory Architecture & Full Struct Definitions

ใน Go ทุก Interface ประกอบด้วย 2 Words บนสถาปัตยกรรม 64-bit (16 Bytes ทั้งหมด) สำหรับตัวแปร error ธรรมดา มันคือ iface ภายใน Runtime:

// runtime/runtime2.go (โครงสร้างระดับล่างของ Go Interface)
type iface struct {
    tab  *itab          // 8 Bytes: ชี้ไปยัง Type Metadata และ Method Table (itab)
    data unsafe.Pointer // 8 Bytes: ชี้ไปยังตัวข้อมูลจริง (Concrete Value / Pointer บน Heap)
}

type itab struct {
    inter *interfacetype // ชี้ไปยังนิยามของ interface (error interface)
    _type *_type         // ชี้ไปยัง Dynamic Type ของ Concrete struct ที่ใส่เข้ามา
    hash  uint32         // Hash ของ _type สำหรับเปรียบเทียบ Type Assertion รวดเร็ว
    _     [4]byte        // Padding 4 Bytes ให้ Memory Aligned 8 Bytes
    fun   [1]uintptr     // Array ของ Function Pointers (ตำแหน่งแรกคือ Error() method)
}

2.1 โครงสร้างไส้ในของ fmt.Errorf("%w")

เมื่อคุณสั่ง fmt.Errorf("context: %w", err) ตัวคอมไพเลอร์และแพ็กเกจ fmt จะสร้าง Instance ของ Struct ภายในชื่อว่า wrapError:

// fmt/errors.go
type wrapError struct {
    msg string // 16 Bytes (8 Bytes Pointer + 8 Bytes Length)
    err error  // 16 Bytes (iface: tab 8 Bytes + data 8 Bytes)
}

func (e *wrapError) Error() string {
    return e.msg
}

// เมธอด Unwrap คือหัวใจที่ errors.Is และ errors.As ใช้แกะกล่อง
func (e *wrapError) Unwrap() error {
    return e.err
}

2.2 โครงสร้าง Multiple Wrapping ใน Go 1.20+

ตั้งแต่ Go 1.20 เป็นต้นมา มีฟังก์ชัน errors.Join(err1, err2) ที่สามารถรวม Error หลายตัวเข้าด้วยกันได้ โดยใช้ Struct ชื่อ joinError:

// errors/join.go
type joinError struct {
    errs []error // 24 Bytes (Pointer 8B + Len 8B + Cap 8B)
}

func (e *joinError) Error() string {
    // Concatenate ข้อความของทุก error ด้วย newline
    ...
}

func (e *joinError) Unwrap() []error {
    return e.errs
}

2.3 Memory Layout ของ Error Interface (16 Bytes)

+-----------------------------------------------------------------------+ | error (iface) = 16 Bytes | +-----------------------------------+-----------------------------------+ | tab (*runtime.itab) [8 Bytes] | data (unsafe.Pointer) [8 Bytes] | | Offset 0x00 | Offset 0x08 | +-----------------+-----------------+-----------------+-----------------+ | | v v +--------------------+ +--------------------+ | runtime.itab | | Actual Concrete | | - inter: error | | Struct Instance | | - _type: *wrapError| | (on Heap / Stack) | | - fun[0]: Error() | +--------------------+ +--------------------+

Section 3: Visual Workflow & Tree Traversal Mechanics

ฟังก์ชัน errors.Is และ errors.As ทำงานอย่างไรในระดับลึก? พวกมันไม่ได้พึ่งพาเวทมนตร์ใดๆ แต่เป็น While-Loop Tree Traversal (สืบหาบรรพบุรุษตาม Interface Contract)

[Incoming Error Chain / Tree] +---------------------------------------------+ | wrapError: "transfer failed: ..." | | tab: *wrapError, data: &wrapError{...} | +---------------------------------------------+ | Unwrap() | (สืบผ่าน Interface) v +---------------------------------------------+ | wrapError: "database txn rollback: ..." | | tab: *wrapError, data: &wrapError{...} | +---------------------------------------------+ | Unwrap() | v +---------------------------------------------+ | Sentinel: ErrInsufficientFunds | | tab: *errors.errorString, data: &errorString| +---------------------------------------------+

3.1 ขั้นตอนการทำงานของ errors.Is(err, target)

  1. Direct Equality: เปรียบเทียบตรงๆ ว่า err == target หรือไม่ ถ้าเท่ากัน ให้คืนค่า true ทันที
  2. Custom Is Check: ตรวจสอบว่า Dynamic Type ของ err ตัวปัจจุบัน implement เมธอด Is(target error) bool หรือไม่ หากมี ให้เรียกใช้เมธอดนั้น
  3. Single Unwrap: ตรวจสอบว่า err implement เมธอด Unwrap() error หรือไม่ หากมี ให้ขยับ Pointer ไปยัง Error ตัวใน แล้ววนกลับไปข้อ 1
  4. Multi-Unwrap (Go 1.20+): หาก implement Unwrap() []error ให้แตกการท่อง Branch แบบ Depth-First Search (DFS) ไปยัง Error ทุกตัวใน Slice
  5. Terminating: หากไม่พบ และไม่สามารถ Unwrap() ได้อีกแล้ว ให้คืนค่า false

3.2 Custom Is Method Pattern (สำหรับ Dynamic Fields)

ในงาน Banking ระบบมักมี Error Code ที่ซับซ้อน เช่น HTTP Status หรือ Error Code ของ Core Ledger ที่ต้องการให้ errors.Is ถือว่าเท่ากันถ้า Error Code ตรงกัน แม้ว่า Message หรือ Timestamp จะต่างกัน:

type BankingLedgerError struct {
    Code      string
    Message   string
    Timestamp int64
}

func (e *BankingLedgerError) Error() string {
    return fmt.Sprintf("[%s] %s (ts:%d)", e.Code, e.Message, e.Timestamp)
}

// Custom Is() ทำให้ errors.Is มีพฤติกรรมเทียบเฉพาะ Field ที่เรากำหนด
func (e *BankingLedgerError) Is(target error) bool {
    t, ok := target.(*BankingLedgerError)
    if !ok {
        return false
    }
    return e.Code == t.Code // สนใจเฉพาะ Code ตรงกัน ถือว่า error ชนิดเดียวกัน!
}

Section 4: Edge Cases, Hazards & Idiomatic Patterns

Hazard #1: The Typed Nil Interface Trap (บั๊กคลาสสิกของ Go)

ใน Go ค่า nil ของ Pointer กับค่า nil ของ Interface คือคนละสิ่งกัน! เมื่อคุณเก็บ (*MyCustomError)(nil) ลงในตัวแปร error ตัว iface.tab จะมีค่า (ชี้ไปยัง Metadata ของ MyCustomError) ขณะที่ iface.data เป็น nil ทำให้การเช็ก err != nil จะได้ผลลัพธ์เป็น TRUE เสมอ!

// ตัวอย่างโค้ดที่ทำให้เกิดหายนะในระบบหักเงิน
func ValidateDeposit(amount int64) error {
    var err *BankingLedgerError = nil // Typed pointer มีค่า nil
    if amount < 0 {
        err = &BankingLedgerError{Code: "ERR_NEGATIVE", Message: "Negative deposit"}
    }
    return err // อันตรายมาก! คืนค่า *BankingLedgerError(nil) ห่อเข้า error interface
}

func TestValidation() {
    err := ValidateDeposit(100)
    // ตรงนี้จะถูกประเมินเป็น TRUE ทันที แม้ amount จะถูกต้อง!
    if err != nil {
        fmt.Println("PANIC: System thinks there is an error because iface.tab != nil!")
    }
}

// วิธีแก้ที่ถูกต้อง (Idiomatic Pattern):
func ValidateDepositFixed(amount int64) error {
    if amount < 0 {
        return &BankingLedgerError{Code: "ERR_NEGATIVE", Message: "Negative deposit"}
    }
    return nil // คืนค่า Untyped nil เสมอ! (iface.tab = nil, iface.data = nil)
}

Idiomatic Pattern: Don't Just Check, Add Value

Section 5: Knowledge Verification Quiz (16 Challenges)

ทดสอบความเข้าใจเชิงลึกระดับ Runtime ทุกข้อมีผลคะแนนสะสมบนแถบด้านบน

Section 6: Hands-on Engineering Labs

เขียนโค้ด Go ให้ผ่านเกณฑ์สถาปัตยกรรม ตรวจสอบ Logic และไวยากรณ์ด้วย In-Browser Verification Engine

Lab 1: Banking Ledger - Sentinel Error & Wrapping

โจทย์: สร้าง Sentinel Error ชื่อ ErrInsufficientBalance ด้วย errors.New จากนั้นเขียนฟังก์ชัน Withdraw(balance, amount int64) error หากยอดเงินไม่พอ ให้ Wrap ด้วย fmt.Errorf พร้อมใส่ Prefix "ledger error: %w"

Lab 2: Race Timing - Custom Struct Error & errors.As Inspection

โจทย์: สร้าง Struct ChipDropError (มีฟิลด์ TagID string, ReaderID int) พร้อม Implement เมธอด Error() string และเขียนฟังก์ชัน InspectDrop(err error) (string, bool) โดยใช้ errors.As เพื่อดึง TagID ออกมา

Lab 3: The Untyped Nil Safety Guard

โจทย์: เขียนฟังก์ชัน AuditTransaction(amount int64) error โดยหลีกเลี่ยงข้อผิดพลาด Typed Nil Interface Trap อย่างเคร่งครัด หากยอดเงินน้อยกว่าหรือเท่ากับ 0 ให้คืนค่า Pointer ของ BankingLedgerError แต่ถ้าปกติ ต้องคืนค่า nil ตัวเปล่าตรงๆ