เรียน Go ภาษาง่าย 📘

🧯

เจาะลึก Error Handling ใน Go ฉบับภาษาง่าย

Is/As/Wrap, sentinel, panic/recover ใช้ให้ถูก

8 บท
12 ข้อสอบ
1. หลักคิด: error คือค่า
go
f, err := os.Open("a.txt")
if err != nil {
    fmt.Println("เปิดไม่ได้:", err)
    return err
}
// ตรงนี้ err == nil แปลว่าปกติ ใช้ f ต่อได้

nil = ว่าง = ไม่พัง กฎเหล็ก: เจอ err เช็คเลย อย่าปล่อยผ่าน อย่า _ ทิ้งถ้าไม่แน่ใจ

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


2. สร้าง error + ห่อด้วย %w
go
import "errors"
errors.New("อายุติดลบไม่ได้")
fmt.Errorf("เปิด %s ไม่ได้: %w", name, err) // %w = ห่ออันเก่าไว้ แกะดูได้
// %v = แค่พิมพ์ %w = ห่อด้วย

ห่อหลายชั้นเหมือนซองจดหมายซ้อนกัน ยังตามกลับไปหาต้นตอได้

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


3. แกะด้วย Is As
go
if errors.Is(err, os.ErrNotExist) {
    // ไฟล์ไม่มี ไม่ใช่พังแบบอื่น
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("พังที่:", pathErr.Path)
}
  • Is = ถามว่าไส้ในใช่คนนี้ไหม (เทียบ sentinel)
  • As = ดึงตัวข้างในออกมาใช้
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


4. Custom error + Sentinel
go
var ErrNoStock = errors.New("ของหมด") // sentinel ไว้ Is

type AgeError struct{ Age int }
func (e *AgeError) Error() string { return fmt.Sprintf("อายุ %d ใช้ไม่ได้", e.Age) }

// คืน: return &AgeError{Age: age}
// เช็ค: var ae *AgeError; if errors.As(err, &ae) { ... }

ทริก: เมธอด Unwrap() error หรือ Is/As แบบกำหนดเองได้ถ้าอยากให้เทียบพิเศษ (ขั้นสูง ข้ามก่อนได้)

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


5. Panic Recover ใช้เมื่อไหร่
  • panic = ชักปลั๊ก ใช้เมื่อไปต่อไม่ได้จริงๆ (config ผิดตั้งแต่เปิด โปรแกรมเมอร์เขียนผิด)
  • งานปกติ (ไฟล์ไม่มี เน็ตล่ม) ให้ return error อย่า panic
  • recover + defer ไว้กันตายทั้งเซิร์ฟเวอร์:
go
defer func() {
    if r := recover(); r != nil {
        slog.Error("กู้ชีพ", "panic", r)
    }
}()
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


6. เช็กลิสต์ production
  • ทุก call นอก (ไฟล์ เน็ต DB) ห่อ %w บอกว่าพังขั้นไหน
  • handler แยก Is not-exist → 404 / ที่เหลือ → 500 อย่าโชว์ stack ให้ user
  • ไม่ log token/รหัสเต็มๆ
  • รัน go vet ./... ก่อนส่ง (จับ %w ผิด / print ผิด)

อ้างอิง: Effective Go #errors, Go 1.27.1 security fixes crypto/x509 net/url os

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ภาคผนวก: ตัวอย่างเพิ่ม + Best Practices
ตัวอย่างเพิ่ม 1: รวมหลาย error ด้วย errors.Join
go
e1 := errors.New("อ่าน config ไม่ได้")
e2 := errors.New("ต่อ DB ไม่ได้")
err := errors.Join(e1, e2)
fmt.Println(err) // พิมพ์สองบรรทัดต่อกัน
if errors.Is(err, e1) {
    fmt.Println("มี e1 ซ่อนอยู่ในนั้น")
}

ใช้ตอน validate หลายช่องพร้อมกัน อยากคืนทุกข้อผิดทีเดียว

ตัวอย่างเพิ่ม 2: แปลง error เป็น HTTP status
go
type StockError struct{ Item string }

func (e *StockError) Error() string { return "ของหมด: " + e.Item }

func statusOf(err error) int {
    if errors.Is(err, os.ErrNotExist) {
        return 404
    }
    var se *StockError
    if errors.As(err, &se) {
        return 409 // ของหมด = conflict
    }
    return 500
}
ตัวอย่างเพิ่ม 3: middleware กัน panic ล้มทั้ง server
go
func safe(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("กู้ panic แล้ว", "err", rec)
                http.Error(w, "server error", http.StatusInternalServerError)
            }
        }()
        h(w, r)
    }
}
Best practices เช็กลิสต์
  • เจอ error เช็คทันที อย่าปล่อยผ่าน อย่า _ ทิ้งถ้าไม่แน่ใจ
  • ข้าม package boundary ให้ห่อ %w ทุกครั้ง จะได้ตามรอยกลับไปหาต้นตอได้
  • เปรียบเทียบด้วย Is/As อย่าใช้ == (ห่อหลายชั้นแล้ว == เทียบไม่เจอ)
  • sentinel (var ErrX = ...) เก็บไว้ที่เดียว ใช้ซ้ำทั้งโปรเจกต์
  • log error แค่ครั้งเดียวที่ชั้นบนสุด (handler/main) ชั้นในแค่ห่อแล้วส่งต่อ
  • อย่าส่งข้อความ internal/stack trace ให้ user เห็น แปลงเป็น code ก่อน
  • มีหลาย error พร้อมกันใช้ errors.Join (เช่น validate ฟอร์มหลายช่อง)
  • panic ใช้เฉพาะ "ไปต่อไม่ได้จริงๆ" (config ผิดตั้งแต่เปิด) งานปกติ return error
แบบฝึกหัด
  • เขียนฟังก์ชันอ่านไฟล์ที่ห่อ %w 3 ชั้น แล้วใช้ Is เช็ก os.ErrNotExist
  • ทำ middleware safe ครอบ handler ที่ panic แล้วเทสต์ว่า server ไม่ตาย