Bài học theo mô-đun (2/5)
Xử lý lỗi thành ngữ
Việc xử lý lỗi trong Go rất rõ ràng: không có ngoại lệ, không thử/bắt. Mọi hàm có thể bị lỗi đều trả về error làm giá trị cuối cùng và người gọi sẽ quyết định. Chương này thu thập các mẫu thành ngữ để tạo ra, truyền bá và kiểm tra lỗi một cách hiệu quả.
Nguyên tắc vàng
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("config %s: %w", path, err)
}Bốn nguyên tắc:
errorlà lần trở lại cuối cùng.- Kiểm tra ngay bằng
if err != nil. - Thêm ngữ cảnh trước khi truyền bá (với
fmt.Errorf+%w). - Không bao giờ bỏ qua lỗi: xử lý, ghi nhật ký hoặc trả lại.
_ = file.Close() // EXPLICITLY ignored (you know what you're doing)Lỗi trọng điểm
lỗi trọng điểm là biến toàn cục được xuất var ErrXxx = errors.New("...") mà người gọi có thể so sánh với:
package store
var ErrNotFound = errors.New("not found")
func Get(key string) (string, error) {
if !exists(key) {
return "", ErrNotFound
}
// ...
}Người gọi sử dụng errors.Is (KHÔNG ==), để thực hiện chuỗi bọc:
v, err := store.Get(k)
if errors.Is(err, store.ErrNotFound) {
// handle 404
}Ví dụ tiêu chuẩn: io.EOF, sql.ErrNoRows, os.ErrNotExist.
Lỗi gõ tùy chỉnh
Để mang dữ liệu có cấu trúc (mã HTTP, trường không hợp lệ, v.v.), hãy xác định loại:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}Người gọi sử dụng errors.As để downcast một cách an toàn dọc theo chuỗi:
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("invalid field:", ve.Field)
}errors.As đi theo chuỗi Unwrap() cho đến khi tìm thấy lỗi có thể gán cho con trỏ được truyền vào.
Gói bằng %w
fmt.Errorf với động từ %w tạo ra lỗi bọc lỗi gốc, giữ nguyên cho errors.Is/errors.As:
if err := db.Query(); err != nil {
return fmt.Errorf("load users: %w", err)
}Nếu không có %w (sử dụng %v hoặc %s), bạn sẽ nhận được chuỗi phong phú hơn nhưng mất chuỗi: người gọi không thể nhận ra các lỗi cụ thể nữa.
Quy tắc dành cho %w:
- Chỉ một cuộc gọi cho mỗi
fmt.Errorfcuộc gọi (yêu cầu nhiều góierrors.Join). - Đối số PHẢI là
error, không phải là một chuỗi. - Thêm ngữ cảnh, không sao chép tin nhắn gốc.
// bad: duplicates
return fmt.Errorf("error: %w", err) // "error: file not found"
// good: useful context
return fmt.Errorf("load config %s: %w", path, err)errors.Join vì nhiều lỗi
Kể từ phiên bản Go 1.20, errors.Join(errs...) kết hợp nhiều lỗi thành một lỗi giúp errors.Is/As có thể phát hiện tất cả các lỗi đó:
var errs []error
for _, item := range items {
if err := process(item); err != nil {
errs = append(errs, fmt.Errorf("item %s: %w", item.ID, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}Khi nào nên sử dụng panic
Không bao giờ có lỗi dự kiến. Chỉ trong ba trường hợp:
- Lỗi không thể phục hồi: một bất biến bị vi phạm ở chỗ "không thể xảy ra" (
panic("unreachable")). - Khởi tạo không thành công trong
init()/main()của một chương trình độc lập (thiếu cấu hình quan trọng → không thành công nhanh). - API thư viện ghi lại tài liệu đó một cách rõ ràng: ví dụ:
regexp.MustCompile(hoảng sợ nếu mẫu không hợp lệ — có thể chấp nhận được vì mẫu là hằng số thời gian xây dựng).
Mọi thứ khác đều là error. Không sử dụng panic làm phím tắt để truyền bá: nó làm hỏng bố cục, bỏ qua defer Close() và khiến người gọi choáng ngợp.
Bài tập
Tóm tắt lỗi của os.ReadFile với fmt.Errorf sử dụng chi tiết %w e aggiungendo il Contesto 'config:' (với đường dẫn).
Giải pháp khả dụng sau 3 lần thử
Lỗi của Hoa Kỳ. Có thể xác minh rằng bạn có lỗi trong danh sách này và đã sửa lỗi cho tất cả các trọng điểm ErrNotFound và đóng dấu 'manca!' trong trường hợp này.
Giải pháp khả dụng sau 3 lần thử
Bạn có thành ngữ nào không? Bạn đang hoảng sợ trong một thư viện Đi?
func F(x int) {
// ??? panic(...)
}