⚡ Go Senior Mentor Engine | Phase 1: Day 6 - Error Handling
Score: 0 / 15
Answered: 0 / 15
Accuracy: 0%
Phase 1: Week 2 Control Flow & Reliability Error as Values

Day 6: Error as Values, Sentinel Errors & Custom Error Types

ปฏิวัติกระบวนการคิดด้าน Reliability: สลัดภาพจำ `try-catch` จาก JVM เข้าใจว่าทำไม Go ถึงมอง Error เป็น Data Value ธรรมดา, ผ่าไส้ในอินเทอร์เฟซ `iface` ของ error (16 Bytes), การประกาศ Sentinel Errors ระดับ Package สำหรับระบบ Core Banking และการสร้าง Custom Error Type พร้อม Metadata

0. The "Why": ทำไมภาษา Go ถึงไม่มี Exception แบบ Java/Kotlin?

ในฝั่ง JVM เมื่อคุณเขียนระบบโอนเงิน คุณมักเขียนแบบนี้:

// Kotlin / Spring Boot style
fun transferMoney(from: String, to: String, amount: Long) {
    if (balance < amount) {
        throw InsufficientFundsException("Account $from has insufficient funds")
    }
    // Logic อื่นๆ
}

เมื่อเกิด throw Exception เบื้องหลัง JVM จะทำ Stack Unwinding: ไล่จับ Stack Trace ทั้งหมด, หยุดการทำงานของ Thread ชั่วคราว, จัดสรร Object ขนาดใหญ่ลง Heap, แล้วกระโดดข้าม Call Stack หลายชั้นขึ้นไปหาบล็อก catch ซึ่งทำให้เกิดปัญหาใหญ่ 2 ประการ:

  1. Hidden Execution Path: คุณอ่านโค้ดบรรทัดหนึ่ง คุณไม่รู้เลยว่ามันจะหลุดออกไปที่ไหน ใครดักจับ หรือหลุดไปถึง Global Exception Handler ของ Spring Boot ทำให้การ Trace บั๊กในระบบ Core Banking ทำได้ยากมาก
  2. Performance Overhead: การสร้าง Stack Trace ของ Java มีค่าใช้จ่าย CPU มหาศาล ในระบบ Ingestion ที่มี Transaction ล้มเหลวบ่อยครั้ง (เช่น Validation Fail) การโยน Exception จะกิน CPU สูงถึง 30-40% โดยเปล่าประโยชน์

ภาษา Go จึงเลือกตัดแนวคิด Exception ทิ้งไป และใช้ปรัชญา "Errors are Values": Error ก็เป็นเพียง "ข้อมูล (Data Value)" ก้อนหนึ่งที่ฟังก์ชันส่งคืนกลับมาคู่กับผลลัพธ์ผ่าน Multi-value return ให้นักพัฒนาตัดสินใจจัดการทันที ณ จุดเกิดเหตุ

คำศัพท์เทคนิคระดับวิศวกรรมสถาปัตยกรรม (Vocabulary):

  • Error as Value: การมองข้อผิดพลาดเป็นตัวแปรธรรมดาที่สามารถส่งผ่าน, ตรวจสอบ (Inspect), เก็บลง Slice หรือบันทึกลง Log ได้เหมือนตัวแปรทั่วไป
  • The `error` Interface: อินเทอร์เฟซมาตรฐานในตัวภาษา Go นิยามเพียง Error() string เมธอดเดียว
  • Sentinel Error: Error ค่าคงที่ระดับแพ็กเกจ (ประกาศด้วย errors.New) ที่ส่งสัญญาณสถานะเฉพาะตัว เช่น ErrInsufficientFunds เพื่อให้ฝั่งเรียกใช้ตรวจสอบด้วยเครื่องหมาย ==
  • Custom Error Type: Struct ที่เราสร้างขึ้นเองและ Implement เมธอด Error() string เพื่อบรรจุ Metadata ทางธุรกิจ เช่น AccountID, DeficitAmount, Timestamp
  • Early Return (Happy Path Left): สไตล์การเขียนโค้ด Go ที่เช็ค if err != nil แล้วจัดการ Error ออกจากฟังก์ชันทันที ทำให้โค้ดส่วน Business Logic ไม่ต้องเยื้องเข้าไปลึกในวงเล็บปีกกา
1. Go Syntax & Command Blueprint: ไวยากรณ์ Error และการใช้งานจริง

เรียนรู้วิธีการเขียนฟังก์ชันคืนค่า Error และการสร้าง Sentinel / Custom Error แบบทีละขั้นตอน:

1.1 ฟังก์ชันที่คืนค่าแบบ Multi-value Return (Result, error)

ใน Go เมื่อฟังก์ชันอาจทำงานล้มเหลว ลายเซ็นต์มาตรฐานคือต้องคืนค่า error เป็นพารามิเตอร์สุดท้ายเสมอ:

package banking

import (
    "errors"
    "fmt"
)

// ฟังก์ชันคืนค่าสองตัว: (ยอดเงินคงเหลือ int64, ข้อผิดพลาด error)
func Withdraw(currentBalance, amount int64) (int64, error) {
    if amount <= 0 {
        // สร้าง error ง่ายๆ ด้วย errors.New
        return currentBalance, errors.New("withdrawal amount must be greater than zero")
    }

    if currentBalance < amount {
        // จัด Format ข้อความ error ด้วย fmt.Errorf
        return currentBalance, fmt.Errorf("insufficient funds: balance is %d, requested %d", currentBalance, amount)
    }

    newBalance := currentBalance - amount
    return newBalance, nil // nil หมายถึงทำงานสำเร็จ ไม่มี Error!
}

1.2 วิธีเรียกใช้และการเขียนสไตล์ Happy Path (Left-aligned)

func ProcessTransaction() {
    balance := int64(1000)

    // สไตล์ Idiomatic Go: รับค่าคู่ แล้วตรวจ if err != nil ทันที
    newBalance, err := Withdraw(balance, 1500)
    if err != nil {
        // จัดการ Error ทันที (เช่น บันทึก Log, Rollback, แจ้งเตือน)
        fmt.Println("Transaction Failed:", err)
        return // Early return จบงาน
    }

    // Happy Path: โค้ดที่ทำงานสำเร็จจะอยู่ชิดซ้าย ไม่อยู่ใน else block!
    fmt.Printf("Withdrawal Success! New Balance: %d\n", newBalance)
}

1.3 การประกาศและการตรวจสอบ Sentinel Errors

นิยมตั้งชื่อขึ้นต้นด้วย Err เป็นตัวแปร Global ระดับ Package เพื่อให้ Caller เช็คเงื่อนไขได้:

package banking

import "errors"

// ประกาศ Sentinel Errors ไว้ที่ระดับ Package
var (
    ErrAccountNotFound   = errors.New("banking: account not found")
    ErrInsufficientFunds = errors.New("banking: insufficient balance")
    ErrAccountLocked     = errors.New("banking: account is locked")
)

func Debit(accountID string, amount int64) error {
    if accountID != "ACC-01" {
        return ErrAccountNotFound
    }
    // สมมติยอดเงินไม่พอ
    return ErrInsufficientFunds
}

func CallDebit() {
    err := Debit("ACC-01", 500)
    // ตรวจสอบด้วยเครื่องหมาย == ตรงๆ
    if err == ErrInsufficientFunds {
        fmt.Println("Please top-up your account")
    }
}

1.4 การสร้าง Custom Error Type พร้อม Metadata

สร้าง Struct ธรรมดา และผูกเมธอด Error() string เข้าไป:

type FundError struct {
    AccountID string
    Balance   int64
    Attempted int64
}

// ผูกเมธอด Error() string เพื่อให้ Struct นี้กลายเป็น error interface โดยอัตโนมัติ!
func (e FundError) Error() string {
    return fmt.Sprintf("account %s deficit: balance %d but attempted %d", 
        e.AccountID, e.Balance, e.Attempted)
}

func ExecuteTransfer(from string, balance, amt int64) error {
    if balance < amt {
        return FundError{
            AccountID: from,
            Balance:   balance,
            Attempted: amt,
        }
    }
    return nil
}
2. Memory Architecture: ไส้ในของ `error` Interface บน RAM

2.1 โครงสร้างของ Non-empty Interface (`iface`)

ใน Go ตัวแปรชนิด error ไม่ใช่พอยน์เตอร์เดี่ยวๆ และไม่ใช่สตริง แต่คือ Interface Container ที่มีขนาด 16 Bytes บนระบบ 64-bit โครงสร้างใต้กระโปรงคือ Struct iface:

// โครงสร้างภายใน Go Runtime (src/runtime/runtime2.go)
type iface struct {
    tab  *itab          // 8 Bytes: ชี้ไปยัง Type Metadata และ Function Pointer ของ Error()
    data unsafe.Pointer // 8 Bytes: ชี้ไปยังข้อมูลจริง (เช่น ชี้ไปที่ String หรือ Custom Struct)
}
+---------------------------------------------------------------------------------------------------+ | ERROR INTERFACE (iface) IN RAM - TOTAL SIZE: 16 BYTES | +---------------------------------------------------------------------------------------------------+ | | | var err error = errors.New("invalid account") | | | | [iface Header on Stack - 16 Bytes] | | ┌─────────────────────────────────┬─────────────────────────────────┐ | | │ tab *itab (8 Bytes) │ data unsafe.Pointer (8 Bytes) │ | | └──────────────┬──────────────────┴────────────────┬────────────────┘ | | │ │ | | ▼ ▼ | | [*itab Type Descriptor] [*errorString Struct on Heap] | | ┌─────────────────────────────┐ ┌────────────────────────────────┐ | | │ Type: *errors.errorString │ │ s: "invalid account" (string) │ | | │ Func: Error() string │ │ - Data: 0x88910 (8 Bytes) │ | | └─────────────────────────────┘ │ - Len: 15 (8 Bytes) │ | | └────────────────────────────────┘ | | | | 🎯 กฎสำคัญ: err == nil จะเป็นจริง ก็ต่อเมื่อ ทั้ง tab == nil และ data == nil ทั้งคู่เท่านั้น! | +---------------------------------------------------------------------------------------------------+

2.2 ไส้ในของ `errors.New` ใน Go Standard Library

มาดูโค้ดจริงของ Standard Library ในไฟล์ src/errors/errors.go:

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}
💡 ทำไม `errors.New` ต้องคืนค่าเป็น Pointer `&errorString{text}`?
เพราะทีมออกแบบ Go ต้องการให้ Sentinel Error 2 ตัวที่มีข้อความเหมือนกันทุกตัวอักษร มีค่าไม่เท่ากันถ้าสร้างคนละครั้ง!
เช่น err1 := errors.New("fail") กับ err2 := errors.New("fail")
เมื่อเทียบ err1 == err2 ผลลัพธ์จะเป็น false เพราะมันเปรียบเทียบ Memory Address ของ data pointer ไม่ใช่การเทียบตัวหนังสือ! ทำให้แต่ละ Package สามารถสร้าง Sentinel Error ของตัวเองได้โดยไม่ชนกับคนอื่น
3. Visual Workflow: เปรียบเทียบ Flow การทำงาน JVM vs Go

พิจารณาลำดับการทำงานเมื่อเกิดข้อผิดพลาดในการโอนเงิน:

+---------------------------------------------------------------------------------------------------+ | EXECUTION FLOW: JVM EXCEPTION vs GO ERROR VALUE | +---------------------------------------------------------------------------------------------------+ | 1. JVM (Stack Unwinding / Heavy Interruption): | | | | CallStack: [Service] ──> [Repository] ──> [DatabaseDriver] | | │ | | ▼ DB Connection Error! | | throw SQLException | | │ | | Stack Unwinding ◄─────────────────────────────────┘ (หยุดเธรด, ไล่เก็บ StackTrace, กิน CPU) | | │ | | ▼ หลุดกระโดดข้าม Repository ไปหา GlobalExceptionHandler ใน Spring Boot ทันที! | | | +---------------------------------------------------------------------------------------------------+ | 2. Go (Linear Control Flow / Zero Overhead): | | | | CallStack: [Service] ──> [Repository] ──> [DatabaseDriver] | | │ | | ▼ DB Connection Error! | | return nil, ErrConnClosed | | │ (ส่งคืนค่า Value 16 ไบต์บน Stack ธรรมดา) | | ▼ | | [Repository] เช็ค: if err != nil ──> return nil, fmt.Errorf("repo: %w", err) | | │ | | ▼ | | [Service] เช็ค: if err != nil ──> ตัดสินใจ Retry / Rollback / Log จบในฟังก์ชัน | | | | 🎯 ข้อได้เปรียบ: ไม่มี Stack Unwinding, คอนโทรลโฟลว์วิ่งเป็นเส้นตรง, ตรวจสอบจุดเกิดเหตุได้ 100% | +---------------------------------------------------------------------------------------------------+
4. Edge Cases, Hazards & Idiomatic Patterns

4.1 กับดักอันตรายอันดับ 1 ของ Go: "The Nil Interface Trap"

นี่คือบั๊กที่วิศวกรที่ย้ายมาจาก Java/Kotlin ตกม้าตายบ่อยที่สุด:

type CustomError struct {
    Code int
}

func (c *CustomError) Error() string {
    return fmt.Sprintf("code: %d", c.Code)
}

// BUG FUNCTION: ประกาศ return type เป็น Custom Pointer แต่ฟังก์ชันภายนอกรับเป็น error interface!
func Process() error {
    var err *CustomError = nil // ตัวแปรพอยน์เตอร์มีค่าเป็น nil

    if someCondition {
        err = &CustomError{Code: 404}
    }

    return err // ⚠️ หายนะเกิดขึ้นตรงนี้!
}

func main() {
    err := Process()
    if err != nil {
        // บรรทัดนี้จะถูกรันเสมอ! แม้ว่า err จะเป็น nil pointer มาก็ตาม!
        fmt.Println("Error occurred:", err) 
    }
}
⚠️ ทำไม `err != nil` ถึงเป็น TRUE?
จำโครงสร้าง iface ได้ไหมครับ? ตัวแปร error คือ Interface Container ที่เก็บ 2 ช่อง:
1. tab: ชี้ไปยังประเภท *CustomError (มีค่า! ไม่ใช่ nil)
2. data: มีค่า nil
ใน Go อินเทอร์เฟซจะมีค่าเท่ากับ nil ก็ต่อเมื่อ ทั้ง Type (`tab`) และ Value (`data`) เป็น nil ทั้งคู่! ในเคสนี้ tab มีข้อมูลประเภทอยู่ ทำให้ err != nil เป็นจริงเสมอ!
กฎเหล็ก: ถ้าฟังก์ชันคืนค่า error และไม่มีความผิดพลาด ให้สั่ง `return nil` เปล่าๆ ออกมาเสมอ ห้ามคืนตัวแปร Custom Struct Pointer ที่เป็น nil!

4.2 กฎการออกแบบ Sentinel Errors ในระบบธนาคาร

Sentinel Error มีไว้สำหรับบอก **"สถานะที่คาดการณ์ได้ว่าจะเกิด"** เช่น ยอดเงินไม่พอ, บัญชีถูกล็อค, บัตรหมดอายุ ห้ามใช้ Sentinel Error กับข้อผิดพลาดเชิงลึกที่ต้องส่งต่อ Dynamic Metadata (เช่น เกิด Network Timeout กี่วินาที) เพราะถ้าใช้ Sentinel Error เราจะไม่สามารถแทรกข้อมูลเพิ่มเติมลงไปได้

5. Knowledge Verification: Master 15-Question Exam (Error Basics)
🛠️ Lab 6.1: Core Banking Withdrawal Sentinel Verification
Sentinel Errors
📋 โจทย์ข้อกำหนด:
  1. ประกาศ Sentinel Error ชื่อ ErrNegativeAmount = errors.New("withdrawal amount must be positive")
  2. ประกาศ Sentinel Error ชื่อ ErrInsufficientFunds = errors.New("insufficient balance for withdrawal")
  3. เขียนฟังก์ชัน ProcessWithdrawal(balance, amount int64) (int64, error):
    • ถ้า amount <= 0 ให้คืนค่า balance, ErrNegativeAmount
    • ถ้า balance < amount ให้คืนค่า balance, ErrInsufficientFunds
    • ถ้าผ่าน ให้คืนค่า balance - amount, nil
🛠️ Lab 6.2: Financial Transaction Custom Error Type
Custom Error Struct
📋 โจทย์ข้อกำหนด:
  1. สร้าง struct ชื่อ TxError มีฟิลด์: TxID string, AccountID string, Code int, และ Message string
  2. Implement เมธอด Error() string ให้กับ TxError โดยคืนค่าในรูปแบบ: "tx [TxID] account [AccountID] error (code Code): Message" (ใช้ fmt.Sprintf)
  3. เขียนฟังก์ชัน ValidateTx(txID, accID string, amount int64) error:
    • ถ้า amount <= 0 คืนค่า Struct TxError ที่มี Code = 400 และ Message = "invalid amount"
    • ถ้าถูกต้อง ให้คืนค่า nil
🛠️ Lab 6.3: Fixing the Nil Interface Hazard
Nil Interface Trap
📋 โจทย์ข้อกำหนด:
  1. โค้ดด้านล่างมีบั๊กมหาศาล: ประกาศตัวแปรพอยน์เตอร์ var appErr *AppError = nil แล้วส่งออกไปผ่าน error interface ทำให้ผลลัพธ์ err != nil เป็นจริงเสมอแม้ไม่มี error
  2. แก้ไของค์ประกอบฟังก์ชัน ExecutePayment(amount int64) error ให้คืนค่า nil เปล่าๆ ออกมาเมื่อไม่มีข้อผิดพลาด