Go 1.13 thêm cơ chế bọc lỗi, và nó đổi hẳn cách viết mã xử lý lỗi. Bài này về ba hàm bạn cần và một cái bẫy một ký tự.
Bọc bằng %w
goc := &LoiKhongTim{Ma: "DH-9"}
t1 := fmt.Errorf("tầng kho: %w", goc)
t2 := fmt.Errorf("tầng api: %w", t1)
chuỗi lỗi: tầng api: tầng kho: không tìm thấy: DH-9
Mỗi tầng thêm ngữ cảnh của mình, và thông điệp cuối cùng đọc như một dấu vết từ ngoài vào trong.
Quan trọng hơn: %w giữ liên kết tới lỗi gốc, tạo thành một chuỗi mà bạn đi ngược lại được.
Cái bẫy một ký tự
fmt.Errorf("dùng %v: %v", goc) // dùng %v thay vì %w
errors.Is với %v = false <<< MẤT liên kết
%v chỉ lấy chuỗi của lỗi. Thông điệp in ra giống hệt, nhưng liên kết đã đứt và errors.Is không tìm thấy gì nữa.
Đây là lỗi im lặng đúng kiểu khó chịu nhất: log trông bình thường, chỉ có logic kiểm lỗi ở tầng trên là sai. Và không có công cụ nào trong thư viện chuẩn cảnh báo — go vet chỉ bắt được khi số lượng đối số sai.
Quy tắc: luôn dùng %w khi bọc lỗi, trừ khi bạn cố tình muốn cắt liên kết (ví dụ để không lộ chi tiết nội bộ ra API công khai).
Chỉ được một %w trong mỗi fmt.Errorf trước Go 1.20; từ 1.20 thì nhiều %w cũng được.
errors.Is: so sánh danh tính
var ErrHetHan = errors.New("hết hạn")
l := fmt.Errorf("gọi API: %w", ErrHetHan)
errors.Is(l, ErrHetHan) = true
l == ErrHetHan = false <<< SAI vì đã bọc
Đây là lý do errors.Is tồn tại. So sánh == chỉ đúng khi lỗi chưa bị bọc — và bạn không kiểm soát được điều đó, vì tầng nào cũng có thể bọc thêm.
errors.Is đi suốt chuỗi và so từng tầng. Nên mã kiểm lỗi của bạn không phụ thuộc vào việc có bao nhiêu tầng đã bọc.
Lỗi thư viện chuẩn cũng dùng được:
errors.Is(err, os.ErrNotExist) = true
Đây là mẫu chuẩn để kiểm "tệp không tồn tại", thay cho việc so chuỗi thông điệp — thứ vừa mong manh vừa phụ thuộc ngôn ngữ hệ thống.
errors.As: lấy lại kiểu cụ thể
Khi lỗi mang dữ liệu bạn cần:
type LoiKhongTim struct{ Ma string }
func (e *LoiKhongTim) Error() string { return "không tìm thấy: " + e.Ma }
var target *LoiKhongTim
if errors.As(t2, &target) {
fmt.Println(target.Ma)
}
errors.As(t2, &target) = true -> lấy lại Ma="DH-9"
errors.As tìm trong chuỗi lỗi cái đầu tiên khớp kiểu, rồi gán vào biến bạn đưa. Chú ý phải truyền con trỏ tới biến, không phải biến.
Đây là cách thay cho catch (LoiKhongTim e) của Java, và nó vẫn hoạt động dù lỗi đã bị bọc năm tầng.
errors.Unwrap: gỡ một tầng
errors.Unwrap(t2) == t1 = true
Hiếm khi cần dùng trực tiếp — Is và As đã gọi nó bên trong. Nhưng biết nó tồn tại giúp hiểu cơ chế: một lỗi "bọc được" là lỗi có method Unwrap() error.
Chọn sentinel hay kiểu lỗi
Hai cách khai báo lỗi để người gọi kiểm:
Sentinel — một biến lỗi cố định:
var ErrHetHan = errors.New("hết hạn")
Dùng khi chỉ cần biết "loại lỗi gì", không cần dữ liệu kèm theo. Kiểm bằng errors.Is.
Kiểu lỗi — struct cài error:
type LoiKhongTim struct{ Ma string }
Dùng khi cần mang dữ liệu. Kiểm bằng errors.As.
Lưu ý thiết kế: cả hai đều trở thành một phần API công khai của bạn. Người dùng sẽ viết errors.Is(err, kho.ErrHetHan), và đổi tên hay xoá nó là phá vỡ tương thích.
Cái bẫy con trỏ nil, lần nữa
Bài 17 đã đo. Nó áp dụng thẳng vào đây:
func f() error {
var e *LoiKhongTim // nil
return e // interface KHÔNG nil
}
Và errors.Is(f(), nil) cho false như tôi đo được. Luôn return nil tường minh.
Thử ba mươi giây
goc := errors.New("gốc")
a := fmt.Errorf("bọc: %w", goc)
b := fmt.Errorf("bọc: %v", goc)
fmt.Println(errors.Is(a, goc), errors.Is(b, goc))
In ra true false. Hai dòng khác nhau đúng một ký tự, và một trong hai làm hỏng toàn bộ logic kiểm lỗi phía trên.
Ngày mai: panic và recover — khi nào được phép, và vì sao hiếm khi đúng.