0

Go Zero to Hero - Bài 42: So sánh lỗi với errors.Is và errors.As

Trước bản Go 1.13, khi một lỗi bị bọc lại bằng fmt.Errorf("... %v", err), nó sẽ trở thành một chuỗi text vô hồn. Kể từ Go 1.13, nhờ sự ra đời của %w, cái lỗi gốc được bảo tồn nguyên vẹn bên trong một cấu trúc dạng Danh sách liên kết (Linked List).

Tuy nhiên, lớp vỏ bọc này làm cho toán tử == trở nên vô dụng khi so sánh lỗi. Đó là lý do errors.Is và errors.As xuất hiện.


1. Nguồn cơn vấn đề: Khi toán tử == và Ép kiểu trở nên vô dụng

Hãy xem điều gì xảy ra khi bạn dùng toán tử == trên một lỗi đã bị bọc:

package main

import (
    "errors"
    "fmt"
)

var ErrTimeout = errors.New("timeout kết nối DB")

func main() {
    // Tầng Repository sinh ra lỗi
    rootErr := ErrTimeout
    
    // Tầng Service bọc lỗi lại
    wrappedErr := fmt.Errorf("lấy dữ liệu vé thất bại: %w", rootErr)
    
    // ❌ TOÁN TỬ == VÔ DỤNG:
    // Mặc dù bản chất là lỗi Timeout, nhưng wrappedErr đã là một object khác.
    fmt.Println(wrappedErr == ErrTimeout) // Kết quả: FALSE
}

Để xuyên thủng các lớp vỏ %w này, chúng ta BẮT BUỘC phải dùng errors.Is hoặc errors.As. Hai hàm này hoạt động bằng cách gọi hàm Unwrap() liên tục để lột từng lớp vỏ của củ hành tây cho đến khi tìm thấy thứ chúng ta cần.


2. errors.Is: Kẻ soi xét "Giá trị" (Value)

errors.Is là sự thay thế hoàn hảo cho toán tử ==. Nó dùng để so sánh xem trong toàn bộ chuỗi lỗi bị bọc, có tồn tại một Sentinel Error (Lỗi toàn cục) cụ thể nào đó hay không.

Nó đặc biệt hữu dụng khi bạn làm việc với các lỗi chuẩn của thư viện Go (như sql.ErrNoRows hoặc os.ErrNotExist).

func main() {
    rootErr := ErrTimeout
    wrappedErr := fmt.Errorf("tầng 1: %w", rootErr)
    doubleWrappedErr := fmt.Errorf("tầng 2: %w", wrappedErr)

    // ✅ errors.Is XUYÊN THẤU MỌI LỚP VỎ
    if errors.Is(doubleWrappedErr, ErrTimeout) {
        fmt.Println("Đã tìm thấy lỗi Timeout ẩn sâu bên trong!")
    }
}

💡 Luật ngầm: Dùng errors.Is khi bạn chỉ quan tâm đến câu hỏi: "Cái lỗi này CÓ PHẢI LÀ cái lỗi kia không?", và bạn KHÔNG CẦN trích xuất thêm bất kỳ dữ liệu (Metadata) nào từ nó.


3. errors.As: Kẻ trích xuất "Kiểu dữ liệu" (Type)

errors.As là phiên bản nâng cấp của Type Assertion (err.(*CustomError)). Nó dùng khi cái lỗi gốc của bạn không phải là một biến errors.New() đơn giản, mà là một Struct tự định nghĩa chứa nhiều trường dữ liệu phức tạp.

Nó giải quyết 2 câu hỏi cùng lúc: "Trong chuỗi lỗi này CÓ KIỂU DỮ LIỆU ĐÓ không? Nếu có, hãy LẤY DỮ LIỆU ĐÓ RA cho tôi."

// Tự định nghĩa một Struct lỗi nghiệp vụ
type AFCError struct {
    MachineID string
    Code      int
}

func (e *AFCError) Error() string {
    return fmt.Sprintf("Lỗi máy %s (Code: %d)", e.MachineID, e.Code)
}

func main() {
    // Khởi tạo lỗi nghiệp vụ và bọc lại
    rootErr := &AFCError{MachineID: "TVM-01", Code: 503}
    wrappedErr := fmt.Errorf("kẹt giấy in biên lai: %w", rootErr)

    // Khai báo một biến HỨNG dữ liệu (phải là con trỏ)
    var targetErr *AFCError

    // ✅ errors.As VỪA KIỂM TRA, VỪA GÁN DỮ LIỆU VÀO targetErr
    if errors.As(wrappedErr, &targetErr) {
        fmt.Printf("Trích xuất thành công! Mã lỗi: %d, Tại máy: %s\n", targetErr.Code, targetErr.MachineID)
    }
}

⚠️ Lưu ý sinh tử: Tham số thứ hai của errors.As BẮT BUỘC phải là Con trỏ trỏ tới một Con trỏ (hoặc con trỏ trỏ tới interface). Nếu bạn truyền một Value bình thường, hàm sẽ lập tức Panic. Trong ví dụ trên, targetErr vốn đã là *AFCError, khi truyền vào errors.As, ta phải thêm & thành &targetErr (nghĩa là **AFCError).


4. Bảng tổng kết: Ranh giới rạch ròi

Để không bao giờ bị lúng túng khi gõ code, hãy lưu lại bảng quy tắc "chọn vũ khí" này:

Tiêu chí errors.Is(err, target) errors.As(err, &target)
Mục đích chính So sánh Giá trị (Value Equality) Kiểm tra Kiểu và Trích xuất (Type Assertion)
Đối tượng áp dụng Sentinel Errors (Các biến lỗi toàn cục) Custom Error Structs (Các Struct lỗi tự định nghĩa)
Tương đương với Toán tử == Cú pháp ép kiểu val, ok := err.(*Type)
Tham số thứ 2 Là một biến lỗi thông thường (error) Bắt buộc là con trỏ của biến cần hứng (&target)
Khi nào nên dùng? Khi chỉ cần biết CÓ lỗi đó xảy ra hay không để rẽ nhánh if-else. Khi cần lôi HTTP Code, MachineID từ trong Struct lỗi ra để xử lý tiếp.

Tóm lại, errors.Is trả lời câu hỏi "Là ai?", còn errors.As trả lời câu hỏi "Thuộc loại nào và mang theo cái gì?".


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.