Error handling

Error handling: Thành thạo cách xử lý lỗi trong Go (error wrapping, custom errors, panic/recover).


🎯 Tư duy về Error trong Go

  • Go không có exception như Java. Thay vào đó, error là một giá trị (error là một interface).

  • Error trong Go phải được kiểm soát có chủ đích, không thể lờ đi.

  • Không dùng panic để xử lý lỗi logic thông thường.


1. 📦 Kiểu error cơ bản

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

Sử dụng:

res, err := divide(10, 0)
if err != nil {
    log.Println("Error:", err)
    return
}
fmt.Println("Result:", res)

2. 🎨 Custom error types

Tạo error có ngữ cảnh riêng và phân loại lỗi:

type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("Validation error on field '%s': %s", e.Field, e.Msg)
}

func validateUsername(username string) error {
    if len(username) < 5 {
        return &ValidationError{Field: "username", Msg: "too short"}
    }
    return nil
}

Xử lý:

err := validateUsername("abc")
if vErr, ok := err.(*ValidationError); ok {
    log.Printf("Validation failed: %v\n", vErr)
}

3. 🧩 Error Wrapping (fmt.Errorf, errors.Join, errors.Unwrap)

Go 1.13+ hỗ trợ error wrapping để giữ lại ngữ cảnh lỗi gốc:

a. Wrapping error

func readFile(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return fmt.Errorf("failed to read file %s: %w", path, err)
    }
    _ = data
    return nil
}

b. Unwrap và check loại lỗi

err := readFile("notfound.txt")
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File not found")
}

4. 🪓 Panic & Recover – Dùng đúng lúc

❌ Không dùng panic trong logic nghiệp vụ

✅ Dùng panic cho exception không recover được: vi phạm hợp đồng, lỗi dev (not user)

⚠️ Recover dùng để chặn hệ thống sập

func safeExecute(f func()) {
    defer func() {
        if r := recover(); r != nil {
            log.Println("Recovered from panic:", r)
        }
    }()
    f()
}

5. 📌 Best Practices từ Senior

✅ Luôn trả lỗi, không panic:

data, err := getData()
if err != nil {
    return nil, fmt.Errorf("getData failed: %w", err)
}

✅ Phân loại lỗi: dùng custom error type hoặc sentinel error

var ErrUserNotFound = errors.New("user not found")

✅ Logging lỗi ở layer cao nhất (ex: handler, main) – tránh log trùng nhiều tầng.

✅ Đừng nuốt lỗi:

_, err := doSomething()
if err != nil {
    // đừng ignore
}

6. 📁 Mô hình hoá Error theo tầng ứng dụng

  • repo layer: return ErrNotFound, ErrDB, custom errors

  • service layer: wrap lại với context

  • handler layer: phân tích lỗi và trả HTTP status tương ứng


7. 🧪 Unit test với error

func TestDivide_ByZero(t *testing.T) {
    _, err := divide(10, 0)
    require.Error(t, err)
    assert.EqualError(t, err, "cannot divide by zero")
}

Kết

Mục tiêu
Công cụ

Ghi log lỗi rõ ràng

log, zap, logrus

Theo dõi lỗi production

Sentry, Honeybadger, OpenTelemetry

Gỡ lỗi, trace

pprof, panic/recover, errors.Wrap

Viết error semantic rõ ràng

Custom errors, errors.Is/As/Unwrap


Last updated