Bài 5: Bắt lỗi với Error Formatting (%w) trong package fmt
Trong các hệ thống phần mềm, một yêu cầu từ người dùng (ví dụ: Tạo tài khoản) thường phải đi qua rất nhiều lớp (layers). Từ HTTP Handler, nó gọi xuống UseCase, UseCase lại gọi xuống Repository (Database), Repository lại gọi một hàm đọc File cấu hình.
Giả sử ở tầng chót cùng (đọc file) xảy ra lỗi "file not found". Nếu lỗi này cứ thế trả ngược lên trên cùng và in ra màn hình, lập trình viên sẽ bối rối: "File nào? Lỗi đọc file này đang nằm ở chức năng Tạo tài khoản hay chức năng Upload ảnh?".
Để giải quyết, Go cung cấp kỹ thuật Error Wrapping (Gói Lỗi) thông qua hàm fmt.Errorf và một Verb đặc biệt: %w.
1. Hàm fmt.Errorf (Khởi tạo lỗi tùy chỉnh)
Trước khi nói về Wrapping, hãy nói về cách tạo ra một đối tượng lỗi (error) cơ bản. Thông thường, bạn có thể dùng errors.New("chỉ chứa text"). Nhưng nếu bạn cần nhét thêm các biến động (như User ID, File Name) vào thông báo lỗi, fmt.Errorf là công cụ bạn cần.
Hàm này hoạt động y hệt fmt.Sprintf (dùng các ký tự định dạng như %s, %d), nhưng thay vì trả về string, nó trả về một đối tượng kiểu error.
Go
package main
import "fmt"
func fetchUser(id int) error {
if id < 0 {
// Tạo một error có định dạng string chèn biến
return fmt.Errorf("id người dùng không hợp lệ: %d. ID phải lớn hơn 0", id)
}
return nil
}
func main() {
err := fetchUser(-5)
if err != nil {
fmt.Println("Lỗi:", err)
}
}
2. Sự ra đời của Error Wrapping (%w)
Trước phiên bản Go 1.13, khi muốn chèn thêm ngữ cảnh vào một lỗi có sẵn (ví dụ: nhận lỗi từ thư viện Database, muốn thêm chữ "Lỗi truy vấn db"), lập trình viên thường dùng %v để nối chuỗi:
fmt.Errorf("lỗi tầng repo: %v", errFromDB)
Vấn đề lớn: Việc dùng %v sẽ biến lỗi gốc (errFromDB) thành một chuỗi văn bản (String) thuần túy. Nó làm MẤT DẤU VẾT kiểu dữ liệu của lỗi gốc. Ở tầng trên cùng, bạn không thể dùng câu lệnh errors.Is(err, sql.ErrNoRows) để kiểm tra chính xác xem lỗi đó có phải là "Không tìm thấy dữ liệu" hay không.
Đó là lý do Go ra mắt Verb %w (Wrap - Gói).
Khi bạn dùng fmt.Errorf("... %w", errGoc), Go sẽ tạo ra một lỗi mới bọc lấy lỗi cũ, giống như một con búp bê Nga (Matryoshka). Thông điệp nối vào nhau, nhưng lỗi bên trong vẫn được giữ nguyên bản chất.
3. Thực hành: Chuỗi cung ứng Lỗi (Error Chain)
Hãy xem cách %w giữ lại ngữ cảnh qua 3 tầng kiến trúc:
Go
package main
import (
"errors"
"fmt"
)
// Khai báo một Sentinel Error (Lỗi tiêu chuẩn ở tầng thấp nhất)
var ErrFileNotFound = errors.New("không tìm thấy file hệ thống")
// Tầng 3: Database/Filesystem (Tầng sâu nhất)
func readConfig() error {
// Trả về lỗi gốc
return ErrFileNotFound
}
// Tầng 2: UseCase (Logic nghiệp vụ)
func initializeSystem() error {
err := readConfig()
if err != nil {
// Dùng %w ĐỂ GÓI LỖI GỐC, đồng thời thêm thông tin của tầng này
return fmt.Errorf("khởi tạo hệ thống thất bại: %w", err)
}
return nil
}
// Tầng 1: HTTP Controller (Nơi tiếp nhận Request ngoài cùng)
func startApp() error {
err := initializeSystem()
if err != nil {
// Tiếp tục gói lỗi một lần nữa
return fmt.Errorf("không thể khởi động App: %w", err)
}
return nil
}
func main() {
err := startApp()
// 1. In ra chuỗi lỗi đầy đủ để Debug
fmt.Println("Thông báo lỗi (cho Dev):")
fmt.Printf("%+v\n\n", err)
// Kết quả: không thể khởi động App: khởi tạo hệ thống thất bại: không tìm thấy file hệ thống
// -> Đọc từ trái sang phải, ta biết rõ hành trình của lỗi.
// 2. SO SÁNH VÀ BÓC TÁCH (Sức mạnh thực sự của %w)
// Dùng errors.Is để xuyên qua 2 lớp vỏ bảo vệ,
// kiểm tra xem lõi bên trong có phải là ErrFileNotFound không.
if errors.Is(err, ErrFileNotFound) {
fmt.Println("Xử lý riêng: File config bị thiếu. Đang tạo file config mặc định...")
} else {
fmt.Println("Báo lỗi chung cho User: Lỗi hệ thống 500")
}
}
Nguyên tắc Vàng trong kiến trúc Backend Go:
- Khi bạn nhận được một
errortừ một hàm khác và muốn đẩy nó lên hàm cha, HÃY LUÔN DÙNGfmt.Errorf("...: %w", err). Đừng bao giờ chỉreturn errtrơ trọi (vì sẽ mất dấu vết hàm nào gây lỗi), và cũng đừng dùng%v(vì sẽ phá hỏng chuỗi lỗi).
Bây giờ bạn đã biết cách xuất dữ liệu, định dạng các kiểu biến phức tạp và gói lỗi gọn gàng. Nhưng có một vấn đề: Làm sao để kiểm soát được cách hàm fmt.Println hay %v in ra cái Struct mà bạn tự định nghĩa? (Ví dụ: Ẩn bớt trường mật khẩu khi in struct User).
Đó là lúc chúng ta bước vào bài cuối cùng với fmt.Stringer.
All rights reserved