Go Zero to Hero - Bài 41: Tự định nghĩa kiểu Lỗi (Custom Errors) với errors.New
Trong các hệ thống backend cấp độ Enterprise, việc chỉ trả về một thông báo lỗi dạng chuỗi đơn thuần là chưa đủ. Bạn cần một cơ chế phân loại, kiểm tra và trích xuất thông tin lỗi (metadata) một cách chính xác. Golang cung cấp các kỹ thuật nâng cao để giải quyết triệt để bài toán này.
1. Biến lỗi toàn cục (Sentinel Errors)
Thay vì mỗi lần gặp lỗi lại gọi errors.New("thẻ hết tiền") rải rác khắp nơi trong code, các kỹ sư Backend thường định nghĩa sẵn những lỗi phổ biến thành các Biến toàn cục (Global Variables) ngay ở đầu package. Kỹ thuật này gọi là tạo ra các Sentinel Errors (Lỗi lính canh).
package main
import (
"errors"
"fmt"
)
// Khai báo các Sentinel Errors ở đầu file/package, thường bắt đầu bằng chữ "Err"
var (
ErrInsufficientFunds = errors.New("số dư trong thẻ không đủ")
ErrCardBlacklisted = errors.New("thẻ nằm trong danh sách đen")
ErrNetworkTimeout = errors.New("mất kết nối đến trạm kiểm soát")
)
func processGatePass(cardID string, balance float64) error {
if cardID == "BLOCKED_123" {
return ErrCardBlacklisted // Trả về biến lỗi đã định nghĩa
}
if balance < 15000 {
return ErrInsufficientFunds
}
return nil
}
func main() {
err := processGatePass("VALID_456", 5000)
// Kiểm tra chính xác xem đây là loại lỗi gì bằng phép so sánh ==
if err == ErrInsufficientFunds {
fmt.Println("Giao diện TVM: Vui lòng nạp thêm tiền vào thẻ!")
} else if err != nil {
fmt.Println("Lỗi hệ thống:", err)
}
}
💡 Ưu điểm: Bằng cách này, bạn có thể dễ dàng dùng lệnh
if err == ErrXđể rẽ nhánh logic xử lý (ví dụ: lỗi hết tiền thì hiện popup, lỗi rớt mạng thì thử lại 3 lần) mà không cần phải so sánh chuỗi ngớ ngẩn nhưif err.Error() == "số dư trong thẻ không đủ".
2. Nâng cấp: Tự định nghĩa Struct Lỗi chứa Metadata
Sentinel Errors rất tốt, nhưng nó tĩnh. Điều gì xảy ra nếu bạn muốn đính kèm Mã trạm (StationID), Mã máy (MachineID), hoặc HTTP Status Code vào trong cái lỗi đó để ghi log cho dễ? errors.New không làm được điều này.
Nhớ lại kiến thức về Struct và Method: Bất kỳ Struct nào sở hữu hàm Error() string thì trình biên dịch đều coi Struct đó là một error. Vậy thì chúng ta cứ việc tự tạo Struct thôi!
// Tự định nghĩa một Struct Lỗi cho hệ thống AFC
type AFCSystemError struct {
Code int // Mã lỗi hệ thống (ví dụ: 404, 500, 4002)
MachineID string // Ghi nhận máy nào đang bị lỗi
Message string // Lời nhắn chi tiết
}
// Implement interface 'error' bằng cách gắn method Error() vào Struct
func (e *AFCSystemError) Error() string {
return fmt.Sprintf("[Máy %s - Lỗi %d] %s", e.MachineID, e.Code, e.Message)
}
// Hàm nghiệp vụ sinh ra Custom Error
func syncDataToCentral(machineID string) error {
// Trả về con trỏ Struct dưới lớp vỏ của interface 'error'
return &AFCSystemError{
Code: 503,
MachineID: machineID,
Message: "Không thể đẩy lịch sử giao dịch lên server trung tâm",
}
}
3. Khai phá bộ đôi errors.Is và errors.As (Go 1.13+)
Ở bài học về Error Wrapping, chúng ta đã biết dùng %w để bọc (Wrap) nhiều tầng lỗi lại với nhau. Nhưng khi lỗi bị bọc lại như một củ hành tây, toán tử == không còn tác dụng nữa!
Ví dụ: err := fmt.Errorf("lỗi tổng: %w", ErrInsufficientFunds). Lúc này err == ErrInsufficientFunds sẽ trả về false.
Để xuyên thủng các lớp vỏ và kiểm tra xem có cái lỗi nào bên trong hay không, Go cung cấp 2 hàm cực kỳ mạnh mẽ:
A. Kiểm tra Sentinel Error bằng errors.Is
Hàm này sẽ lột từng lớp vỏ của biến lỗi gốc, và so sánh xem có cái ruột nào khớp với Sentinel Error bạn cần tìm hay không.
func main() {
// Lỗi gốc bị bọc lại
wrappedErr := fmt.Errorf("không thể xử lý khách hàng: %w", ErrInsufficientFunds)
// Dùng errors.Is thay cho ==
if errors.Is(wrappedErr, ErrInsufficientFunds) {
fmt.Println("Đã phát hiện lỗi thiếu tiền dù bị bọc nhiều lớp!")
}
}
B. Trích xuất Custom Error Struct bằng errors.As
Nếu errors.Is dùng để kiểm tra "giá trị", thì errors.As dùng để trích xuất "kiểu dữ liệu" (giống như Type Assertion, nhưng an toàn và xuyên thấu qua các lớp bọc).
Bạn dùng nó để lôi cái Struct AFCSystemError ra khỏi biến error chung chung, từ đó truy cập được vào biến Code và MachineID.
func main() {
err := syncDataToCentral("TVM-ST1-001")
// Bọc lỗi lại một lớp nữa ở tầng trên
finalErr := fmt.Errorf("Job Sync thất bại lúc nửa đêm: %w", err)
// Khai báo một biến rỗng chứa con trỏ của kiểu lỗi ta muốn lấy ra
var targetErr *AFCSystemError
// errors.As sẽ quét các lớp bọc của finalErr, nếu tìm thấy AFCSystemError,
// nó sẽ gán dữ liệu vào targetErr và trả về true.
if errors.As(finalErr, &targetErr) {
fmt.Printf("Báo động đỏ! Máy %s đang gặp mã lỗi %d\n", targetErr.MachineID, targetErr.Code)
} else {
fmt.Println("Lỗi không xác định:", finalErr)
}
}
Tổng kết
Bạn đã nắm trong tay những vũ khí mạnh nhất để thiết kế hệ thống báo lỗi chuẩn Enterprise:
- Sentinel Errors (
errors.New): Định nghĩa các biến lỗi toàn cục để tái sử dụng và kiểm tra bằngerrors.Is. - Custom Errors: Tự thiết kế các Struct chứa
Code,MachineIDvà implement hàmError() stringđể mang theo Metadata. - Bóc vỏ (
errors.As): Công cụ thần thánh để trích xuất ngược Custom Error Struct từ một lỗi đã bị bọc (%w) nhiều tầng, giúp bạn dễ dàng làm việc với HTTP Status Code hoặc gán cờ Retry khi thiết kế Microservices.
All rights reserved