⚠️ Xử Lý Sự Cố - Error Handling
Chào mừng đến với bài học về Error Handling - cách xử lý sự cố trong bếp nhà hàng!
🎯 Món Ăn Hôm Nay
Tưởng tượng bạn đang nấu phở và gặp sự cố:
- Hết muối → Phải xử lý: Đi mua hoặc dùng nước mắm thay thế
- Nước dùng cạn → Phải xử lý: Thêm nước nhanh
- Thịt bị hỏng → Phải xử lý: Báo khách, đổi món khác
Đó chính là Error Handling - xử lý sự cố để món ăn vẫn hoàn hảo!
🥘 Error Handling Là Gì?
Error = Sự cố trong bếp (thứ không mong muốn xảy ra) Error Handling = Cách xử lý sự cố
// Nấu món có thể gặp sự cố
func nauPho(khoThit int) (string, error) {
    if khoThit < 100 {
        return "", errors.New("❌ Thiếu thịt! Cần ít nhất 100g")
    }
    return "🍜 Phở Bò", nil  // nil = không có lỗi
}
// Sử dụng
mon, err := nauPho(50)
if err != nil {
    fmt.Println("Sự cố:", err)
    return
}
fmt.Println("Thành công:", mon)
🍜 Ẩn Dụ Nhà Hàng:
- error = Sự cố trong bếp
- nil = Mọi thứ ổn
- if err != nil = Kiểm tra có sự cố không
👨🍳 Câu Chuyện Trong Bếp
Sáng nay, nhà hàng gặp nhiều sự cố:
Tình huống 1: Kiểm tra nguyên liệu
func kiemTraNguyenLieu(tenNL string, soLuong int) error {
    if soLuong < 100 {
        return fmt.Errorf("❌ Thiếu %s! Còn %dg, cần ít nhất 100g", tenNL, soLuong)
    }
    return nil
}
// Sử dụng
if err := kiemTraNguyenLieu("Thịt bò", 50); err != nil {
    fmt.Println(err)
    // Xử lý: Đi mua hoặc đổi món
}
Tình huống 2: Xử lý nhiều sự cố
func chuanBiMon(tenMon string) error {
    // Kiểm tra nguyên liệu
    if err := kiemTraKho(tenMon); err != nil {
        return fmt.Errorf("lỗi kho: %w", err)  // Bọc lỗi
    }
    // Kiểm tra đầu bếp
    if err := kiemTraDauBep(); err != nil {
        return fmt.Errorf("lỗi nhân sự: %w", err)
    }
    return nil
}
🎉 Kết quả: Error handling giúp phát hiện và xử lý sự cố kịp thời!
📝 Công Thức Nấu (Code Examples)
Ví Dụ 1: Error Cơ Bản
package main
import (
    "errors"
    "fmt"
)
func nauPho(khoThit int, khoNuocDung int) (string, error) {
    // Kiểm tra thịt
    if khoThit < 100 {
        return "", errors.New("thiếu thịt")
    }
    // Kiểm tra nước dùng
    if khoNuocDung < 500 {
        return "", errors.New("thiếu nước dùng")
    }
    return "🍜 Phở Bò ngon", nil
}
func main() {
    mon, err := nauPho(80, 600)
    if err != nil {
        fmt.Println("❌ Sự cố:", err)
        fmt.Println("→ Cần đi mua thêm nguyên liệu!")
        return
    }
    fmt.Println("✅", mon)
}
Ví Dụ 2: Custom Error - Lỗi Tùy Chỉnh
// Tạo kiểu lỗi riêng
type LoiThieuNguyenLieu struct {
    TenNL    string
    SoLuong  int
    YeuCau   int
}
func (e *LoiThieuNguyenLieu) Error() string {
    return fmt.Sprintf("Thiếu %s: còn %dg, cần %dg",
        e.TenNL, e.SoLuong, e.YeuCau)
}
func kiemTraKho(tenNL string, soLuong int) error {
    yeuCau := 100
    if soLuong < yeuCau {
        return &LoiThieuNguyenLieu{
            TenNL:   tenNL,
            SoLuong: soLuong,
            YeuCau:  yeuCau,
        }
    }
    return nil
}
func main() {
    err := kiemTraKho("Thịt bò", 50)
    if err != nil {
        fmt.Println("❌", err)
        // Kiểm tra loại lỗi cụ thể
        if loiNL, ok := err.(*LoiThieuNguyenLieu); ok {
            thieu := loiNL.YeuCau - loiNL.SoLuong
            fmt.Printf("→ Cần mua thêm %dg %s\n", thieu, loiNL.TenNL)
        }
    }
}
Ví Dụ 3: Error Wrapping - Bọc Lỗi
func layNguyenLieuTuKho(tenNL string) error {
    // Giả sử lỗi từ tầng dưới
    return errors.New("kho đóng cửa")
}
func chuanBiNguyenLieu(tenNL string) error {
    err := layNguyenLieuTuKho(tenNL)
    if err != nil {
        // Bọc lỗi với thêm thông tin
        return fmt.Errorf("không lấy được %s: %w", tenNL, err)
    }
    return nil
}
func nauMon(tenMon string) error {
    err := chuanBiNguyenLieu("thịt bò")
    if err != nil {
        return fmt.Errorf("lỗi nấu %s: %w", tenMon, err)
    }
    return nil
}
func main() {
    err := nauMon("Phở Bò")
    if err != nil {
        fmt.Println("Chuỗi lỗi:", err)
        // Output: lỗi nấu Phở Bò: không lấy được thịt bò: kho đóng cửa
        // Kiểm tra lỗi gốc
        if errors.Is(err, errors.New("kho đóng cửa")) {
            fmt.Println("→ Cần chờ kho mở cửa")
        }
    }
}
Ví Dụ 4: Defer và Error
import "os"
func docCongThuc(tenFile string) error {
    file, err := os.Open(tenFile)
    if err != nil {
        return fmt.Errorf("không mở được công thức: %w", err)
    }
    defer file.Close()  // Đảm bảo đóng file
    // Đọc nội dung
    // ...
    return nil
}
func main() {
    if err := docCongThuc("pho.txt"); err != nil {
        fmt.Println("❌", err)
        fmt.Println("→ Sử dụng công thức dự phòng")
    }
}
Ví Dụ 5: Panic và Recover - Sự Cố Nghiêm Trọng
func nauMon() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("🚨 SỰ CỐ NGHIÊM TRỌNG:", r)
            fmt.Println("→ Gọi bếp trưởng ngay!")
        }
    }()
    // Giả sử có sự cố nghiêm trọng
    panic("Bếp cháy!")
}
func main() {
    fmt.Println("Bắt đầu nấu...")
    nauMon()
    fmt.Println("Tiếp tục hoạt động (đã xử lý)")
}
🔥 Thực Hành Trong Bếp
Bài Tập 1: Hệ Thống Đặt Món
package main
import (
    "errors"
    "fmt"
)
var (
    ErrHetMon     = errors.New("hết món")
    ErrHetNguyenLieu = errors.New("hết nguyên liệu")
)
type QuanAn struct {
    tonKho map[string]int
}
func (q *QuanAn) DatMon(tenMon string, soLuong int) error {
    if soLuong <= 0 {
        return errors.New("số lượng phải > 0")
    }
    kho, coMon := q.tonKho[tenMon]
    if !coMon {
        return fmt.Errorf("%w: %s", ErrHetMon, tenMon)
    }
    if kho < soLuong {
        return fmt.Errorf("%w: %s (còn %d)", ErrHetNguyenLieu, tenMon, kho)
    }
    q.tonKho[tenMon] -= soLuong
    fmt.Printf("✅ Đặt %s x%d thành công\n", tenMon, soLuong)
    return nil
}
func main() {
    quan := &QuanAn{
        tonKho: map[string]int{
            "Phở Bò":   10,
            "Bún Chả":  5,
        },
    }
    // Test các trường hợp
    if err := quan.DatMon("Phở Bò", 3); err != nil {
        fmt.Println("Lỗi:", err)
    }
    if err := quan.DatMon("Cơm Tấm", 1); err != nil {
        if errors.Is(err, ErrHetMon) {
            fmt.Println("→ Món này không có trong menu")
        }
    }
    if err := quan.DatMon("Bún Chả", 10); err != nil {
        if errors.Is(err, ErrHetNguyenLieu) {
            fmt.Println("→ Cần bổ sung nguyên liệu")
        }
    }
}
Bài Tập 2: Xử Lý Thanh Toán
type LoiThanhToan struct {
    SoTien     int
    SoDu       int
    ThieuBaoNhieu int
}
func (e *LoiThanhToan) Error() string {
    return fmt.Sprintf("Không đủ tiền: cần %d VND, còn %d VND (thiếu %d VND)",
        e.SoTien, e.SoDu, e.ThieuBaoNhieu)
}
func thanhToan(soTien int, soDu int) error {
    if soDu < soTien {
        return &LoiThanhToan{
            SoTien:     soTien,
            SoDu:       soDu,
            ThieuBaoNhieu: soTien - soDu,
        }
    }
    fmt.Printf("✅ Thanh toán %d VND thành công\n", soTien)
    return nil
}
func main() {
    err := thanhToan(150000, 100000)
    if err != nil {
        if loiTT, ok := err.(*LoiThanhToan); ok {
            fmt.Println(loiTT)
            fmt.Printf("→ Vui lòng nạp thêm %d VND\n", loiTT.ThieuBaoNhieu)
        }
    }
}
⚠️ Những Lỗi Đầu Bếp Thường Gặp
Lỗi 1: Không Kiểm Tra Error
// ❌ SAI: Bỏ qua lỗi
func main() {
    mon, _ := nauPho(50)  // Bỏ qua lỗi nguy hiểm!
    fmt.Println(mon)
}
// ✅ ĐÚNG: Luôn kiểm tra
func main() {
    mon, err := nauPho(50)
    if err != nil {
        fmt.Println("Lỗi:", err)
        return
    }
    fmt.Println(mon)
}
Lỗi 2: Error Message Không Rõ Ràng
// ❌ SAI: Thông báo mơ hồ
return errors.New("lỗi")
// ✅ ĐÚNG: Thông báo cụ thể
return fmt.Errorf("thiếu nguyên liệu '%s': cần %dg, còn %dg",
    tenNL, yeuCau, tonKho)
Lỗi 3: Panic Không Cần Thiết
// ❌ SAI: Panic cho lỗi thường
func nauPho(khoThit int) string {
    if khoThit < 100 {
        panic("thiếu thịt")  // Quá mức!
    }
    return "Phở"
}
// ✅ ĐÚNG: Trả error bình thường
func nauPho(khoThit int) (string, error) {
    if khoThit < 100 {
        return "", errors.New("thiếu thịt")
    }
    return "Phở", nil
}
💡 Bí Quyết Của Đầu Bếp
- Luôn kiểm tra error: if err != nillà bắt buộc
- Error message chi tiết: Giúp debug nhanh
- Wrap errors: Dùng %wđể giữ lỗi gốc
- Custom errors cho business logic: Dễ xử lý riêng
- Panic chỉ cho lỗi nghiêm trọng: Bếp cháy, database sập,...
🎓 Bạn Đã Học Được
- ✅ Error = Sự cố cần xử lý
- ✅ nil = Không có lỗi
- ✅ errors.New() = Tạo lỗi đơn giản
- ✅ fmt.Errorf("%w") = Bọc lỗi có context
- ✅ Custom error = Lỗi tùy chỉnh cho business
- ✅ panic/recover = Xử lý sự cố nghiêm trọng
🍜 Món Tiếp Theo
Đã biết xử lý sự cố! Giờ học cách test món ăn:
💡 Lời Khuyên Cuối: Error handling giống như kỹ năng xử lý sự cố của đầu bếp - càng nhanh và chính xác, món ăn càng hoàn hảo! Luôn chuẩn bị cho mọi tình huống!