Đây là thứ gây tranh cãi nhất về Go, và là thứ người mới phàn nàn đầu tiên: if err != nil lặp đi lặp lại khắp nơi.
Bài này giải thích vì sao, và cho thấy nó không tệ như vẻ ngoài.
error chỉ là một interface
type error interface {
Error() string
}
Đó là toàn bộ. Một method, trả về chuỗi.
không tìm thấy: DH-1 | kiểu thật *main.LoiKhongTim
Nghĩa là lỗi trong Go là giá trị bình thường: gán được vào biến, truyền được vào hàm, cất được vào slice, so sánh được. Không có cơ chế ngôn ngữ đặc biệt nào cả.
Tạo lỗi có hai cách cơ bản:
errors.New("hết hạn")
fmt.Errorf("không đọc được %s: %w", ten, err)
Vì sao không dùng exception
Lập luận của nhóm Go, và tôi thấy nó có lý sau khi viết Go một thời gian:
Lỗi hiện ra trong chữ ký hàm. func Doc() ([]byte, error) nói rõ hàm này có thể hỏng. Ở Java, unchecked exception không xuất hiện ở đâu cả, và bạn chỉ biết khi nó nổ.
Đường xử lý lỗi là đường mã bình thường. Không có nhánh vô hình nhảy qua nhiều tầng ngăn xếp. Đọc từ trên xuống là thấy hết.
Bỏ qua lỗi phải cố ý. Bài 7 đã đo: nhận thiếu giá trị trả về là lỗi biên dịch. Muốn lờ đi thì phải gõ _. Ở Java, nuốt exception là không làm gì.
Cái giá rất thật: mã dài hơn. Một hàm gọi bốn thứ có thể hỏng sẽ có bốn khối if err != nil. Không có cách nào tránh, và nhóm Go đã từ chối nhiều đề xuất thêm cú pháp tắt.
Bốn mẫu bạn sẽ gõ hằng ngày
Trả thẳng lên:
if err != nil {
return err
}
Thêm ngữ cảnh rồi trả lên — đây là mẫu nên dùng mặc định:
if err != nil {
return fmt.Errorf("đọc cấu hình %s: %w", ten, err)
}
Dấu %w giữ liên kết tới lỗi gốc. Bài mai sẽ nói vì sao nó quan trọng.
Xử lý rồi tiếp tục:
if err != nil {
log.Printf("bỏ qua dòng hỏng: %v", err)
continue
}
Bỏ qua có chủ đích:
_ = f.Close() // rõ ràng là đang bỏ qua
Quy ước viết thông điệp lỗi
Thư viện chuẩn theo bốn quy tắc, và cộng đồng theo rất chặt:
Chữ thường, không dấu chấm cuối. "không tìm thấy tệp" chứ không phải "Không tìm thấy tệp.". Lý do: lỗi thường bị bọc, và bạn muốn chuỗi ghép lại đọc được như một câu.
Không lặp lại từ "error" hay "lỗi". fmt.Errorf("lỗi khi đọc: %w", err) cho ra lỗi khi đọc: lỗi khi mở: .... Viết fmt.Errorf("đọc cấu hình: %w", err).
Thêm ngữ cảnh mà tầng dưới không biết. Tầng đọc tệp biết "không mở được"; chỉ tầng của bạn biết "đang nạp cấu hình cho dịch vụ X".
Kèm dữ liệu giúp gỡ lỗi — tên tệp, mã đơn hàng, chỉ số dòng.
Đừng vừa log vừa trả lên
Đây là lỗi phổ biến nhất trong mã Go thật:
if err != nil {
log.Printf("hỏng: %v", err) // <- rồi
return err // <- lại trả lên
}
Lỗi đó sẽ được log ở mọi tầng nó đi qua. Một lỗi, năm dòng log giống nhau.
Quy tắc: hoặc xử lý, hoặc trả lên — không làm cả hai. Chỉ tầng ngoài cùng (handler HTTP, main) mới log.
error là giá trị nên làm được nhiều thứ
Vì lỗi chỉ là giá trị, bạn dùng nó theo những cách exception không cho:
var loi []error
for _, v := range ds {
if err := xuLy(v); err != nil {
loi = append(loi, err)
}
}
return errors.Join(loi...) // Go 1.20+: gộp nhiều lỗi thành một
errors.Join cho phép trả về nhiều lỗi cùng lúc, và errors.Is vẫn tìm được qua từng cái.
Thử ba mươi giây
Tìm trong dự án Go của bạn:
grep -rn 'log\..*err' --include='*.go' | grep -A1 'return err'
Mỗi chỗ vừa log vừa return err là một lỗi sẽ xuất hiện nhiều lần trong log sản xuất. Xoá dòng log đi và để tầng ngoài cùng lo — bạn vừa làm log sạch hơn.
Ngày mai: errors.Is, errors.As và wrapping — cách kiểm lỗi cho đúng.